diff --git a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs index 43708a7b249..620e5fb6f2b 100644 --- a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs +++ b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs @@ -44,7 +44,9 @@ public class EntityFrameworkRelationalServicesBuilder : EntityFrameworkServicesB public static readonly IDictionary RelationalServices = new Dictionary { - { typeof(IKeyValueIndexFactorySource), new ServiceCharacteristics(ServiceLifetime.Singleton) }, + { typeof(IRowKeyValueFactoryFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) }, + { typeof(IRowForeignKeyValueFactoryFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) }, + { typeof(IRowIndexValueFactoryFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IParameterNameGeneratorFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IComparer), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IMigrationsIdGenerator), new ServiceCharacteristics(ServiceLifetime.Singleton) }, @@ -125,7 +127,9 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices() TryAdd(); TryAdd, ModificationCommandComparer>(); TryAdd(); - 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..7e63709e4b1 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,40 @@ public sealed record RelationalModelDependencies /// the constructor at any point in this process. /// [EntityFrameworkInternal] - public RelationalModelDependencies() + public RelationalModelDependencies( + IRowKeyValueFactoryFactory rowKeyValueFactoryFactory, + IRowForeignKeyValueFactoryFactory foreignKeyRowValueFactorySource, + IRowIndexValueFactoryFactory rowIndexValueFactoryFactory) { + RowKeyValueFactoryFactory = rowKeyValueFactoryFactory; + RowForeignKeyValueFactoryFactory = foreignKeyRowValueFactorySource; + RowIndexValueFactoryFactory = rowIndexValueFactoryFactory; } + + /// + /// This is an internal API that supports the 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; } + + /// + /// This is an internal API that supports the 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 IRowIndexValueFactoryFactory RowIndexValueFactoryFactory { get; init; } } diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index 848021babc3..d861d5d0153 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..cb173e86cfa 100644 --- a/src/EFCore.Relational/Metadata/IColumn.cs +++ b/src/EFCore.Relational/Metadata/IColumn.cs @@ -21,7 +21,7 @@ public interface IColumn : IColumnBase /// /// Gets the property mappings. /// - new IEnumerable PropertyMappings { get; } + new IReadOnlyList PropertyMappings { get; } /// /// Gets the maximum length of data that is allowed in this column. For example, if the property is a ' @@ -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); @@ -148,6 +147,25 @@ public virtual string? Collation => PropertyMappings.First().Property .GetCollation(StoreObjectIdentifier.Table(Table.Name, Table.Schema)); + /// + /// Returns the property mapping for the given entity type. + /// + /// An entity type. + /// The property mapping or if not found. + public virtual IColumnMapping? FindColumnMapping(IReadOnlyEntityType entityType) + { + for (var i = 0; i < PropertyMappings.Count; i++) + { + var mapping = PropertyMappings[i]; + if (mapping.Property.DeclaringEntityType.IsAssignableFrom(entityType)) + { + return mapping; + } + } + + return null; + } + /// /// /// Creates a human-readable representation of the given metadata. diff --git a/src/EFCore.Relational/Metadata/IColumnBase.cs b/src/EFCore.Relational/Metadata/IColumnBase.cs index 5489e0bdb06..4588f10abcd 100644 --- a/src/EFCore.Relational/Metadata/IColumnBase.cs +++ b/src/EFCore.Relational/Metadata/IColumnBase.cs @@ -21,6 +21,11 @@ public interface IColumnBase : IAnnotatable /// string StoreType { get; } + /// + /// Gets the provider type. + /// + Type ProviderClrType { get; } + /// /// Gets the value indicating whether the column can contain NULL. /// @@ -34,5 +39,5 @@ public interface IColumnBase : IAnnotatable /// /// Gets the property mappings. /// - IEnumerable PropertyMappings { get; } + IReadOnlyList PropertyMappings { get; } } diff --git a/src/EFCore.Relational/Metadata/IForeignKeyConstraint.cs b/src/EFCore.Relational/Metadata/IForeignKeyConstraint.cs index 18c0b4ea011..b72c01a2f07 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. @@ -78,11 +83,11 @@ string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOpt .Append(' ') .Append(Table.Name) .Append(' ') - .Append(ColumnBase.Format(Columns)) + .Append(ColumnBase.Format(Columns)) .Append(" -> ") .Append(PrincipalTable.Name) .Append(' ') - .Append(ColumnBase.Format(PrincipalColumns)); + .Append(ColumnBase.Format(PrincipalColumns)); if (OnDeleteAction != ReferentialAction.NoAction) { diff --git a/src/EFCore.Relational/Metadata/IFunctionColumn.cs b/src/EFCore.Relational/Metadata/IFunctionColumn.cs index 8cf75f68b96..6ff09a72e94 100644 --- a/src/EFCore.Relational/Metadata/IFunctionColumn.cs +++ b/src/EFCore.Relational/Metadata/IFunctionColumn.cs @@ -21,7 +21,7 @@ public interface IFunctionColumn : IColumnBase /// /// Gets the property mappings. /// - new IEnumerable PropertyMappings { get; } + new IReadOnlyList PropertyMappings { get; } /// /// diff --git a/src/EFCore.Relational/Metadata/ISqlQueryColumn.cs b/src/EFCore.Relational/Metadata/ISqlQueryColumn.cs index 175572e2bb9..485f567dd59 100644 --- a/src/EFCore.Relational/Metadata/ISqlQueryColumn.cs +++ b/src/EFCore.Relational/Metadata/ISqlQueryColumn.cs @@ -22,7 +22,7 @@ public interface ISqlQueryColumn : IColumnBase /// /// Gets the property mappings. /// - new IEnumerable PropertyMappings { get; } + new IReadOnlyList PropertyMappings { get; } /// /// diff --git a/src/EFCore.Relational/Metadata/ITable.cs b/src/EFCore.Relational/Metadata/ITable.cs index cd8a0b342c6..cd2e7c59de0 100644 --- a/src/EFCore.Relational/Metadata/ITable.cs +++ b/src/EFCore.Relational/Metadata/ITable.cs @@ -33,6 +33,11 @@ public interface ITable : ITableBase /// IEnumerable ForeignKeyConstraints { get; } + /// + /// Gets the foreign key constraints referencing this table. + /// + IEnumerable ReferencingForeignKeyConstraints { get; } + /// /// Gets the unique constraints including the primary key for this table. /// diff --git a/src/EFCore.Relational/Metadata/ITableBase.cs b/src/EFCore.Relational/Metadata/ITableBase.cs index c8547fa2ddb..c71763545eb 100644 --- a/src/EFCore.Relational/Metadata/ITableBase.cs +++ b/src/EFCore.Relational/Metadata/ITableBase.cs @@ -21,6 +21,12 @@ public interface ITableBase : IAnnotatable /// string? Schema { get; } + /// + /// Gets the schema-qualified name of the table in the database. + /// + string SchemaQualifiedName + => Schema == null ? Name : Schema + "." + Name; + /// /// Gets the database model. /// diff --git a/src/EFCore.Relational/Metadata/IUniqueConstraint.cs b/src/EFCore.Relational/Metadata/IUniqueConstraint.cs index 7b87ae8ebe7..6df46ae6295 100644 --- a/src/EFCore.Relational/Metadata/IUniqueConstraint.cs +++ b/src/EFCore.Relational/Metadata/IUniqueConstraint.cs @@ -68,7 +68,7 @@ string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOpt builder .Append(Name) .Append(' ') - .Append(ColumnBase.Format(Columns)); + .Append(ColumnBase.Format(Columns)); if (GetIsPrimaryKey()) { diff --git a/src/EFCore.Relational/Metadata/IViewColumn.cs b/src/EFCore.Relational/Metadata/IViewColumn.cs index ebdc515b117..a43bb1e76b8 100644 --- a/src/EFCore.Relational/Metadata/IViewColumn.cs +++ b/src/EFCore.Relational/Metadata/IViewColumn.cs @@ -21,7 +21,7 @@ public interface IViewColumn : IColumnBase /// /// Gets the property mappings. /// - new IEnumerable PropertyMappings { get; } + new IReadOnlyList PropertyMappings { get; } /// /// diff --git a/src/EFCore.Relational/Metadata/Internal/Column.cs b/src/EFCore.Relational/Metadata/Internal/Column.cs index 70ead4124db..ddec4c1e143 100644 --- a/src/EFCore.Relational/Metadata/Internal/Column.cs +++ b/src/EFCore.Relational/Metadata/Internal/Column.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; /// @@ -9,8 +12,11 @@ namespace Microsoft.EntityFrameworkCore.Metadata.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 Column : ColumnBase, IColumn +public class Column : ColumnBase, IColumn { + // Warning: Never access these fields directly as access needs to be thread-safe + private ColumnAccessors? _accessors; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -31,6 +37,17 @@ public Column(string name, string type, Table table) public new virtual Table Table => (Table)base.Table; + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ColumnAccessors Accessors + => NonCapturingLazyInitializer.EnsureInitialized( + ref _accessors, this, static column => + ColumnAccessorsFactory.Create(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 @@ -48,9 +65,9 @@ ITable IColumn.Table } /// - IEnumerable IColumn.PropertyMappings + IReadOnlyList IColumn.PropertyMappings { [DebuggerStepThrough] - get => PropertyMappings.Cast(); + get => PropertyMappings; } } diff --git a/src/EFCore.Relational/Metadata/Internal/ColumnBase.cs b/src/EFCore.Relational/Metadata/Internal/ColumnBase.cs index 13e1b63f3ec..8e59938a61b 100644 --- a/src/EFCore.Relational/Metadata/Internal/ColumnBase.cs +++ b/src/EFCore.Relational/Metadata/Internal/ColumnBase.cs @@ -9,8 +9,11 @@ namespace Microsoft.EntityFrameworkCore.Metadata.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 ColumnBase : Annotatable, IColumnBase +public class ColumnBase : Annotatable, IColumnBase + where TColumnMappingBase : class, IColumnMappingBase { + private Type? _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 @@ -71,8 +74,49 @@ public override bool IsReadOnly /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual SortedSet PropertyMappings { get; } - = new(ColumnMappingBaseComparer.Instance); + public virtual Type ProviderClrType + { + get + { + if (_providerClrType != null) + { + return _providerClrType; + } + + var typeMapping = PropertyMappings.First().TypeMapping; + var providerType = typeMapping.Converter?.ProviderClrType ?? typeMapping.ClrType; + + return _providerClrType = providerType; + } + } + + /// + /// This is an internal API that supports the 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 List PropertyMappings { get; } + = 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 bool AddPropertyMapping(TColumnMappingBase columnMapping) + { + if (PropertyMappings.IndexOf(columnMapping, ColumnMappingBaseComparer.Instance) != -1) + { + return false; + } + + PropertyMappings.Add(columnMapping); + PropertyMappings.Sort(ColumnMappingBaseComparer.Instance); + + return true; + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -88,7 +132,7 @@ public static string Format(IEnumerable columns) + "}"; /// - IEnumerable IColumnBase.PropertyMappings + IReadOnlyList IColumnBase.PropertyMappings { [DebuggerStepThrough] get => PropertyMappings; diff --git a/src/EFCore.Relational/Metadata/Internal/ColumnMapping.cs b/src/EFCore.Relational/Metadata/Internal/ColumnMapping.cs index 721750c7b23..b3a38460409 100644 --- a/src/EFCore.Relational/Metadata/Internal/ColumnMapping.cs +++ b/src/EFCore.Relational/Metadata/Internal/ColumnMapping.cs @@ -29,8 +29,13 @@ public ColumnMapping( public new virtual ITableMapping TableMapping => (ITableMapping)base.TableMapping; - /// - public override RelationalTypeMapping TypeMapping + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override RelationalTypeMapping GetTypeMapping() => Property.FindRelationalTypeMapping( StoreObjectIdentifier.Table(TableMapping.Table.Name, TableMapping.Table.Schema))!; diff --git a/src/EFCore.Relational/Metadata/Internal/ColumnMappingBase.cs b/src/EFCore.Relational/Metadata/Internal/ColumnMappingBase.cs index 77c71cacd5f..1f241a327f5 100644 --- a/src/EFCore.Relational/Metadata/Internal/ColumnMappingBase.cs +++ b/src/EFCore.Relational/Metadata/Internal/ColumnMappingBase.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.Internal; + namespace Microsoft.EntityFrameworkCore.Metadata.Internal; /// @@ -11,6 +13,8 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal; /// public class ColumnMappingBase : Annotatable, IColumnMappingBase { + private RelationalTypeMapping? _typeMapping; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -19,8 +23,8 @@ public class ColumnMappingBase : Annotatable, IColumnMappingBase /// public ColumnMappingBase( IProperty property, - ColumnBase column, - TableMappingBase tableMapping) + IColumnBase column, + ITableMappingBase tableMapping) { Property = property; Column = column; @@ -30,16 +34,21 @@ public ColumnMappingBase( /// public virtual IProperty Property { get; } + /// + public virtual IColumnBase Column { get; } + + /// + public virtual RelationalTypeMapping TypeMapping => + NonCapturingLazyInitializer.EnsureInitialized( + ref _typeMapping, this, static mapping => mapping.GetTypeMapping()); + /// /// This is an internal API that supports the 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 ColumnBase Column { get; } - - /// - public virtual RelationalTypeMapping TypeMapping + protected virtual RelationalTypeMapping GetTypeMapping() => Property.GetRelationalTypeMapping(); /// @@ -48,7 +57,7 @@ public virtual RelationalTypeMapping TypeMapping /// any release. You should only use it directly in 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 TableMappingBase TableMapping { get; } + public virtual ITableMappingBase TableMapping { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -57,14 +66,7 @@ public virtual RelationalTypeMapping TypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override bool IsReadOnly - => TableMapping.IsReadOnly; - - /// - IColumnBase IColumnMappingBase.Column - { - [DebuggerStepThrough] - get => Column; - } + => ((AnnotatableBase)TableMapping).IsReadOnly; /// ITableMappingBase IColumnMappingBase.TableMapping diff --git a/src/EFCore.Relational/Metadata/Internal/ColumnMappingBaseComparer.cs b/src/EFCore.Relational/Metadata/Internal/ColumnMappingBaseComparer.cs index 85e0eeacbf3..32bd80aa5e6 100644 --- a/src/EFCore.Relational/Metadata/Internal/ColumnMappingBaseComparer.cs +++ b/src/EFCore.Relational/Metadata/Internal/ColumnMappingBaseComparer.cs @@ -64,19 +64,7 @@ public int Compare(IColumnMappingBase? x, IColumnMappingBase? y) return result; } - result = EntityTypeFullNameComparer.Instance.Compare(x.TableMapping.EntityType, y.TableMapping.EntityType); - if (result != 0) - { - return result; - } - - result = StringComparer.Ordinal.Compare(x.Column.Table.Name, y.Column.Table.Name); - if (result != 0) - { - return result; - } - - return StringComparer.Ordinal.Compare(x.Column.Table.Schema, y.Column.Table.Schema); + return TableMappingBaseComparer.Instance.Compare(x.TableMapping, y.TableMapping); } /// @@ -86,7 +74,10 @@ public int Compare(IColumnMappingBase? x, IColumnMappingBase? y) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public bool Equals(IColumnMappingBase? x, IColumnMappingBase? y) - => ReferenceEquals(x, y) || x is not null && y is not null && x.Property == y.Property && x.Column == y.Column; + => ReferenceEquals(x, y) + || (x is not null && y is not null + && x.Property == y.Property && x.Column == y.Column + && TableMappingBaseComparer.Instance.Equals(x.TableMapping, y.TableMapping)); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -97,11 +88,9 @@ public bool Equals(IColumnMappingBase? x, IColumnMappingBase? y) public int GetHashCode(IColumnMappingBase obj) { var hashCode = new HashCode(); - hashCode.Add(obj.Property.Name); - hashCode.Add(obj.Column.Name); - hashCode.Add(obj.Property.DeclaringEntityType, EntityTypeFullNameComparer.Instance); - hashCode.Add(obj.Column.Table.Name); - hashCode.Add(obj.Column.Table.Schema); + hashCode.Add(obj.Property); + hashCode.Add(obj.Column); + hashCode.Add(obj.TableMapping, TableMappingBaseComparer.Instance); return hashCode.ToHashCode(); } 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/ForeignKeyConstraintComparer.cs b/src/EFCore.Relational/Metadata/Internal/ForeignKeyConstraintComparer.cs index a3263c8795f..1f1a54d6c45 100644 --- a/src/EFCore.Relational/Metadata/Internal/ForeignKeyConstraintComparer.cs +++ b/src/EFCore.Relational/Metadata/Internal/ForeignKeyConstraintComparer.cs @@ -90,8 +90,8 @@ public int GetHashCode(IForeignKeyConstraint obj) hashCode.Add(obj.Name); hashCode.Add(obj.Columns, ColumnListComparer.Instance); hashCode.Add(obj.PrincipalColumns, ColumnListComparer.Instance); - hashCode.Add(obj.Table.Name); hashCode.Add(obj.PrincipalTable.Name); + hashCode.Add(obj.Table.Name); return hashCode.ToHashCode(); } } diff --git a/src/EFCore.Relational/Metadata/Internal/FunctionColumn.cs b/src/EFCore.Relational/Metadata/Internal/FunctionColumn.cs index 89b00ef1630..ff077234962 100644 --- a/src/EFCore.Relational/Metadata/Internal/FunctionColumn.cs +++ b/src/EFCore.Relational/Metadata/Internal/FunctionColumn.cs @@ -9,7 +9,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.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 FunctionColumn : ColumnBase, IFunctionColumn +public class FunctionColumn : ColumnBase, IFunctionColumn { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -48,9 +48,9 @@ IStoreFunction IFunctionColumn.Function } /// - IEnumerable IFunctionColumn.PropertyMappings + IReadOnlyList IFunctionColumn.PropertyMappings { [DebuggerStepThrough] - get => PropertyMappings.Cast(); + get => PropertyMappings; } } diff --git a/src/EFCore.Relational/Metadata/Internal/FunctionColumnMapping.cs b/src/EFCore.Relational/Metadata/Internal/FunctionColumnMapping.cs index 04af21264e8..ec3c11cb122 100644 --- a/src/EFCore.Relational/Metadata/Internal/FunctionColumnMapping.cs +++ b/src/EFCore.Relational/Metadata/Internal/FunctionColumnMapping.cs @@ -29,8 +29,13 @@ public FunctionColumnMapping( public virtual IFunctionMapping FunctionMapping => (IFunctionMapping)TableMapping; - /// - public override RelationalTypeMapping TypeMapping + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override RelationalTypeMapping GetTypeMapping() => Property.FindRelationalTypeMapping( StoreObjectIdentifier.DbFunction(FunctionMapping.DbFunction.Name))!; diff --git a/src/EFCore.Relational/Metadata/Internal/FunctionMapping.cs b/src/EFCore.Relational/Metadata/Internal/FunctionMapping.cs index 8eb7bd6b9a0..dba228f71fc 100644 --- a/src/EFCore.Relational/Metadata/Internal/FunctionMapping.cs +++ b/src/EFCore.Relational/Metadata/Internal/FunctionMapping.cs @@ -9,7 +9,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.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 FunctionMapping : TableMappingBase, IFunctionMapping +public class FunctionMapping : TableMappingBase, IFunctionMapping { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -46,6 +46,6 @@ public override string ToString() IEnumerable IFunctionMapping.ColumnMappings { [DebuggerStepThrough] - get => ColumnMappings.Cast(); + get => ColumnMappings; } } diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs index 2a2e6c775e5..7b266507996 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs @@ -150,7 +150,7 @@ public static IRelationalModel Create( foreach (var table in databaseModel.Tables.Values) { - PopulateRowInternalForeignKeys(table); + PopulateRowInternalForeignKeys(table); PopulateTableConfiguration(table, designTime); if (relationalAnnotationProvider != null) @@ -170,11 +170,6 @@ public static IRelationalModel Create( index.AddAnnotations(relationalAnnotationProvider.For(index, designTime)); } - foreach (var constraint in table.ForeignKeyConstraints.Values) - { - constraint.AddAnnotations(relationalAnnotationProvider.For(constraint, designTime)); - } - if (designTime) { foreach (var checkConstraint in table.CheckConstraints.Values) @@ -187,6 +182,19 @@ public static IRelationalModel Create( { ((AnnotatableBase)trigger).AddAnnotations(relationalAnnotationProvider.For(trigger, designTime)); } + } + } + + foreach (var table in databaseModel.Tables.Values) + { + PopulateForeignKeyConstraints(table); + + if (relationalAnnotationProvider != null) + { + foreach (var constraint in table.ForeignKeyConstraints) + { + constraint.AddAnnotations(relationalAnnotationProvider.For(constraint, designTime)); + } table.AddAnnotations(relationalAnnotationProvider.For(table, designTime)); } @@ -194,7 +202,7 @@ public static IRelationalModel Create( foreach (var view in databaseModel.Views.Values) { - PopulateRowInternalForeignKeys(view); + PopulateRowInternalForeignKeys(view); if (relationalAnnotationProvider != null) { @@ -251,7 +259,7 @@ private static void AddDefaultMappings(RelationalModel databaseModel, IEntityTyp { var mappedType = entityType; Check.DebugAssert(entityType.FindRuntimeAnnotationValue(RelationalAnnotationNames.DefaultMappings) == null, "not null"); - var tableMappings = new List(); + var tableMappings = new List>(); entityType.AddRuntimeAnnotation(RelationalAnnotationNames.DefaultMappings, tableMappings); var isTpc = entityType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy; @@ -265,7 +273,7 @@ private static void AddDefaultMappings(RelationalModel databaseModel, IEntityTyp databaseModel.DefaultTables.Add(mappedTableName, defaultTable); } - var tableMapping = new TableMappingBase(entityType, defaultTable, includesDerivedTypes: !isTpc && mappedType == entityType) + var tableMapping = new TableMappingBase(entityType, defaultTable, includesDerivedTypes: !isTpc && mappedType == entityType) { // Table splitting is not supported for default mapping IsSharedTablePrincipal = true, @@ -282,10 +290,10 @@ private static void AddDefaultMappings(RelationalModel databaseModel, IEntityTyp continue; } - var column = (ColumnBase?)defaultTable.FindColumn(columnName); + var column = (ColumnBase?)defaultTable.FindColumn(columnName); if (column == null) { - column = new ColumnBase(columnName, property.GetColumnType(), defaultTable) + column = new ColumnBase(columnName, property.GetColumnType(), defaultTable) { IsNullable = property.IsColumnNullable() }; @@ -297,8 +305,8 @@ private static void AddDefaultMappings(RelationalModel databaseModel, IEntityTyp } var columnMapping = new ColumnMappingBase(property, column, tableMapping); - tableMapping.ColumnMappings.Add(columnMapping); - column.PropertyMappings.Add(columnMapping); + tableMapping.AddColumnMapping(columnMapping); + column.AddPropertyMapping(columnMapping); if (property.FindRuntimeAnnotationValue(RelationalAnnotationNames.DefaultColumnMappings) is not SortedSet columnMappings) @@ -310,7 +318,7 @@ private static void AddDefaultMappings(RelationalModel databaseModel, IEntityTyp columnMappings.Add(columnMapping); } - if (tableMapping.ColumnMappings.Count != 0 + if (((ITableMappingBase)tableMapping).ColumnMappings.Any() || tableMappings.Count == 0) { tableMappings.Add(tableMapping); @@ -394,8 +402,8 @@ private static void AddTables(RelationalModel databaseModel, IEntityType entityT } var columnMapping = new ColumnMapping(property, column, tableMapping); - tableMapping.ColumnMappings.Add(columnMapping); - column.PropertyMappings.Add(columnMapping); + tableMapping.AddColumnMapping(columnMapping); + column.AddPropertyMapping(columnMapping); if (property.FindRuntimeAnnotationValue(RelationalAnnotationNames.TableColumnMappings) is not SortedSet columnMappings) @@ -407,7 +415,7 @@ private static void AddTables(RelationalModel databaseModel, IEntityType entityT columnMappings.Add(columnMapping); } - if (tableMapping.ColumnMappings.Count != 0 + if (((ITableMappingBase)tableMapping).ColumnMappings.Any() || tableMappings.Count == 0) { tableMappings.Add(tableMapping); @@ -491,8 +499,8 @@ private static void AddViews(RelationalModel databaseModel, IEntityType entityTy } var columnMapping = new ViewColumnMapping(property, column, viewMapping); - viewMapping.ColumnMappings.Add(columnMapping); - column.PropertyMappings.Add(columnMapping); + viewMapping.AddColumnMapping(columnMapping); + column.AddPropertyMapping(columnMapping); if (property.FindRuntimeAnnotationValue(RelationalAnnotationNames.ViewColumnMappings) is not SortedSet columnMappings) @@ -504,7 +512,7 @@ private static void AddViews(RelationalModel databaseModel, IEntityType entityTy columnMappings.Add(columnMapping); } - if (viewMapping.ColumnMappings.Count != 0 + if (((ITableMappingBase)viewMapping).ColumnMappings.Any() || viewMappings.Count == 0) { viewMappings.Add(viewMapping); @@ -594,8 +602,8 @@ private static void AddSqlQueries(RelationalModel databaseModel, IEntityType ent } var columnMapping = new SqlQueryColumnMapping(property, column, queryMapping); - queryMapping.ColumnMappings.Add(columnMapping); - column.PropertyMappings.Add(columnMapping); + queryMapping.AddColumnMapping(columnMapping); + column.AddPropertyMapping(columnMapping); if (property.FindRuntimeAnnotationValue(RelationalAnnotationNames.SqlQueryColumnMappings) is not SortedSet columnMappings) @@ -616,7 +624,7 @@ private static void AddSqlQueries(RelationalModel databaseModel, IEntityType ent entityType.AddRuntimeAnnotation(RelationalAnnotationNames.SqlQueryMappings, queryMappings); } - if (queryMapping.ColumnMappings.Count != 0 + if (((ITableMappingBase)queryMapping).ColumnMappings.Any() || queryMappings.Count == 0) { queryMappings.Add(queryMapping); @@ -661,7 +669,7 @@ private static void AddMappedFunctions(RelationalModel databaseModel, IEntityTyp entityType.AddRuntimeAnnotation(RelationalAnnotationNames.FunctionMappings, functionMappings); } - if (functionMapping.ColumnMappings.Count != 0 + if (((ITableMappingBase)functionMapping).ColumnMappings.Any() || functionMappings.Count == 0) { functionMappings.Add(functionMapping); @@ -744,8 +752,8 @@ private static FunctionMapping CreateFunctionMapping( } var columnMapping = new FunctionColumnMapping(property, column, functionMapping); - functionMapping.ColumnMappings.Add(columnMapping); - column.PropertyMappings.Add(columnMapping); + functionMapping.AddColumnMapping(columnMapping); + column.AddPropertyMapping(columnMapping); if (property.FindRuntimeAnnotationValue(RelationalAnnotationNames.FunctionColumnMappings) is not SortedSet columnMappings) @@ -799,117 +807,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); @@ -1041,13 +938,14 @@ private static void PopulateTableConfiguration(Table table, bool designTime) } } - private static void PopulateRowInternalForeignKeys(TableBase table) + private static void PopulateRowInternalForeignKeys(TableBase table) + where TColumnMapping : class, IColumnMappingBase { SortedDictionary>? internalForeignKeyMap = null; SortedDictionary>? referencingInternalForeignKeyMap = null; - TableMappingBase? mainMapping = null; + TableMappingBase? mainMapping = null; var mappedEntityTypes = new HashSet(); - foreach (TableMappingBase entityTypeMapping in ((ITableBase)table).EntityTypeMappings) + foreach (TableMappingBase entityTypeMapping in table.EntityTypeMappings) { var entityType = entityTypeMapping.EntityType; mappedEntityTypes.Add(entityType); @@ -1165,6 +1063,146 @@ private static void PopulateRowInternalForeignKeys(TableBase table) table.OptionalEntityTypes = optionalTypes; } + else + { + table.OptionalEntityTypes = table.EntityTypeMappings.ToDictionary(etm => etm.EntityType, et => false); + } + } + + 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; + var constraint = table.ForeignKeyConstraints.FirstOrDefault(fk => fk.Name == name); + if (constraint != null) + { + 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(constraint); + principalTable.ReferencingForeignKeyConstraints.Add(constraint); + break; + } + } + } } /// diff --git a/src/EFCore.Relational/Metadata/Internal/SqlQueryColumn.cs b/src/EFCore.Relational/Metadata/Internal/SqlQueryColumn.cs index c1b9a529d6c..fcf44b197b4 100644 --- a/src/EFCore.Relational/Metadata/Internal/SqlQueryColumn.cs +++ b/src/EFCore.Relational/Metadata/Internal/SqlQueryColumn.cs @@ -9,7 +9,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.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 SqlQueryColumn : ColumnBase, ISqlQueryColumn +public class SqlQueryColumn : ColumnBase, ISqlQueryColumn { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -48,9 +48,9 @@ ISqlQuery ISqlQueryColumn.SqlQuery } /// - IEnumerable ISqlQueryColumn.PropertyMappings + IReadOnlyList ISqlQueryColumn.PropertyMappings { [DebuggerStepThrough] - get => PropertyMappings.Cast(); + get => PropertyMappings; } } diff --git a/src/EFCore.Relational/Metadata/Internal/SqlQueryColumnMapping.cs b/src/EFCore.Relational/Metadata/Internal/SqlQueryColumnMapping.cs index 5ef439a76a5..f94fbf9dd79 100644 --- a/src/EFCore.Relational/Metadata/Internal/SqlQueryColumnMapping.cs +++ b/src/EFCore.Relational/Metadata/Internal/SqlQueryColumnMapping.cs @@ -29,8 +29,13 @@ public SqlQueryColumnMapping( public virtual ISqlQueryMapping SqlQueryMapping => (ISqlQueryMapping)TableMapping; - /// - public override RelationalTypeMapping TypeMapping + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override RelationalTypeMapping GetTypeMapping() => Property.FindRelationalTypeMapping( StoreObjectIdentifier.SqlQuery(SqlQueryMapping.SqlQuery.Name))!; diff --git a/src/EFCore.Relational/Metadata/Internal/SqlQueryMapping.cs b/src/EFCore.Relational/Metadata/Internal/SqlQueryMapping.cs index 1ea1c5fd311..0453028c964 100644 --- a/src/EFCore.Relational/Metadata/Internal/SqlQueryMapping.cs +++ b/src/EFCore.Relational/Metadata/Internal/SqlQueryMapping.cs @@ -9,7 +9,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.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 SqlQueryMapping : TableMappingBase, ISqlQueryMapping +public class SqlQueryMapping : TableMappingBase, ISqlQueryMapping { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -45,6 +45,6 @@ public override string ToString() IEnumerable ISqlQueryMapping.ColumnMappings { [DebuggerStepThrough] - get => ColumnMappings.Cast(); + get => ColumnMappings; } } diff --git a/src/EFCore.Relational/Metadata/Internal/Table.cs b/src/EFCore.Relational/Metadata/Internal/Table.cs index 7dd967f32a1..7fc96c7b31c 100644 --- a/src/EFCore.Relational/Metadata/Internal/Table.cs +++ b/src/EFCore.Relational/Metadata/Internal/Table.cs @@ -22,7 +22,7 @@ public class Table : TableBase, ITable public Table(string name, string? schema, RelationalModel model) : base(name, schema, model) { - Columns = new SortedDictionary(new ColumnNameComparer(this)); + Columns = new(new ColumnNameComparer(this)); } /// @@ -31,8 +31,17 @@ public Table(string name, string? schema, RelationalModel model) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual SortedDictionary ForeignKeyConstraints { get; } - = new(); + public virtual SortedSet ForeignKeyConstraints { get; } + = new(ForeignKeyConstraintComparer.Instance); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual SortedSet ReferencingForeignKeyConstraints { get; } + = new(ForeignKeyConstraintComparer.Instance); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -168,7 +177,13 @@ IEnumerable ITable.Columns IEnumerable ITable.ForeignKeyConstraints { [DebuggerStepThrough] - get => ForeignKeyConstraints.Values; + get => ForeignKeyConstraints; + } + + IEnumerable ITable.ReferencingForeignKeyConstraints + { + [DebuggerStepThrough] + get => ReferencingForeignKeyConstraints; } /// diff --git a/src/EFCore.Relational/Metadata/Internal/TableBase.cs b/src/EFCore.Relational/Metadata/Internal/TableBase.cs index 5dff0aa590a..6c735468904 100644 --- a/src/EFCore.Relational/Metadata/Internal/TableBase.cs +++ b/src/EFCore.Relational/Metadata/Internal/TableBase.cs @@ -63,7 +63,7 @@ public override bool IsReadOnly /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual SortedSet EntityTypeMappings { get; } + public virtual SortedSet EntityTypeMappings { get; } = new(TableMappingBaseComparer.Instance); /// @@ -72,7 +72,7 @@ public override bool IsReadOnly /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual SortedDictionary Columns { get; protected set; } + public virtual SortedDictionary Columns { get; protected set; } = new(StringComparer.Ordinal); /// diff --git a/src/EFCore.Relational/Metadata/Internal/TableIndex.cs b/src/EFCore.Relational/Metadata/Internal/TableIndex.cs index 318a1c97453..31866e8084d 100644 --- a/src/EFCore.Relational/Metadata/Internal/TableIndex.cs +++ b/src/EFCore.Relational/Metadata/Internal/TableIndex.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; /// @@ -76,6 +79,19 @@ public virtual IReadOnlyList? IsDescending public virtual string? Filter => MappedIndexes.First().GetFilter(StoreObjectIdentifier.Table(Table.Name, Table.Schema)); + private IRowIndexValueFactory? _rowIndexValueFactory; + + /// + /// This is an internal API that supports the 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 IRowIndexValueFactory GetRowIndexValueFactory() + => NonCapturingLazyInitializer.EnsureInitialized( + ref _rowIndexValueFactory, this, + static constraint => constraint.Table.Model.Model.GetRelationalDependencies().RowIndexValueFactoryFactory.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 diff --git a/src/EFCore.Relational/Metadata/Internal/TableMapping.cs b/src/EFCore.Relational/Metadata/Internal/TableMapping.cs index 50fed714fe8..09c8e2a2ec2 100644 --- a/src/EFCore.Relational/Metadata/Internal/TableMapping.cs +++ b/src/EFCore.Relational/Metadata/Internal/TableMapping.cs @@ -9,7 +9,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.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 TableMapping : TableMappingBase, ITableMapping +public class TableMapping : TableMappingBase, ITableMapping { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -49,6 +49,6 @@ ITableBase ITableMappingBase.Table IEnumerable ITableMapping.ColumnMappings { [DebuggerStepThrough] - get => ColumnMappings.Cast(); + get => ColumnMappings; } } diff --git a/src/EFCore.Relational/Metadata/Internal/TableMappingBase.cs b/src/EFCore.Relational/Metadata/Internal/TableMappingBase.cs index 8737add532e..619a422333d 100644 --- a/src/EFCore.Relational/Metadata/Internal/TableMappingBase.cs +++ b/src/EFCore.Relational/Metadata/Internal/TableMappingBase.cs @@ -9,7 +9,8 @@ namespace Microsoft.EntityFrameworkCore.Metadata.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 TableMappingBase : Annotatable, ITableMappingBase +public class TableMappingBase : Annotatable, ITableMappingBase + where TColumnMapping : class, IColumnMappingBase { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -53,8 +54,27 @@ public override bool IsReadOnly /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual SortedSet ColumnMappings { get; } - = new(ColumnMappingBaseComparer.Instance); + protected virtual List ColumnMappings { get; } + = 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 bool AddColumnMapping(TColumnMapping columnMapping) + { + if (ColumnMappings.IndexOf(columnMapping, ColumnMappingBaseComparer.Instance) != -1) + { + return false; + } + + ColumnMappings.Add(columnMapping); + ColumnMappings.Sort(ColumnMappingBaseComparer.Instance); + + return true; + } /// public virtual bool IncludesDerivedTypes { get; } diff --git a/src/EFCore.Relational/Metadata/Internal/UniqueConstraint.cs b/src/EFCore.Relational/Metadata/Internal/UniqueConstraint.cs index 547bbd08a72..eec6a5c4422 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,19 @@ public UniqueConstraint( public override bool IsReadOnly => Table.Model.IsReadOnly; + private IRowKeyValueFactory? _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() + => 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 diff --git a/src/EFCore.Relational/Metadata/Internal/ViewColumn.cs b/src/EFCore.Relational/Metadata/Internal/ViewColumn.cs index bb00d23e4d9..635fd1185bb 100644 --- a/src/EFCore.Relational/Metadata/Internal/ViewColumn.cs +++ b/src/EFCore.Relational/Metadata/Internal/ViewColumn.cs @@ -9,7 +9,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.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 ViewColumn : ColumnBase, IViewColumn +public class ViewColumn : ColumnBase, IViewColumn { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -48,9 +48,9 @@ IView IViewColumn.View } /// - IEnumerable IViewColumn.PropertyMappings + IReadOnlyList IViewColumn.PropertyMappings { [DebuggerStepThrough] - get => PropertyMappings.Cast(); + get => PropertyMappings; } } diff --git a/src/EFCore.Relational/Metadata/Internal/ViewColumnMapping.cs b/src/EFCore.Relational/Metadata/Internal/ViewColumnMapping.cs index 5d15a0ccfd2..5d2d2f5082a 100644 --- a/src/EFCore.Relational/Metadata/Internal/ViewColumnMapping.cs +++ b/src/EFCore.Relational/Metadata/Internal/ViewColumnMapping.cs @@ -29,8 +29,13 @@ public ViewColumnMapping( public virtual IViewMapping ViewMapping => (IViewMapping)TableMapping; - /// - public override RelationalTypeMapping TypeMapping + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override RelationalTypeMapping GetTypeMapping() => Property.FindRelationalTypeMapping( StoreObjectIdentifier.View(ViewMapping.View.Name, ViewMapping.View.Schema))!; diff --git a/src/EFCore.Relational/Metadata/Internal/ViewMapping.cs b/src/EFCore.Relational/Metadata/Internal/ViewMapping.cs index 4197e3bdbb3..0d92331d077 100644 --- a/src/EFCore.Relational/Metadata/Internal/ViewMapping.cs +++ b/src/EFCore.Relational/Metadata/Internal/ViewMapping.cs @@ -9,7 +9,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.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 ViewMapping : TableMappingBase, IViewMapping +public class ViewMapping : TableMappingBase, IViewMapping { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -42,6 +42,6 @@ public override string ToString() IEnumerable IViewMapping.ColumnMappings { [DebuggerStepThrough] - get => ColumnMappings.Cast(); + get => ColumnMappings; } } diff --git a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs index 7e9ebc3ba4e..d61d58aa2ba 100644 --- a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs +++ b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs @@ -1122,7 +1122,7 @@ private void Initialize( { throw new InvalidOperationException( RelationalStrings.DefaultValueUnspecified( - (column.Table.Name, column.Table.Schema).FormatTable(), + column.Table.SchemaQualifiedName, column.Name)); } @@ -1130,7 +1130,7 @@ private void Initialize( { throw new InvalidOperationException( RelationalStrings.DefaultValueSqlUnspecified( - (column.Table.Name, column.Table.Schema).FormatTable(), + column.Table.SchemaQualifiedName, column.Name)); } @@ -1139,7 +1139,7 @@ private void Initialize( throw new InvalidOperationException( RelationalStrings.ComputedColumnSqlUnspecified( column.Name, - (column.Table.Name, column.Table.Schema).FormatTable())); + column.Table.SchemaQualifiedName)); } var property = column.PropertyMappings.First().Property; diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index dfc6f727e99..0ca37357f9e 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 differing 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}'). /// @@ -1001,6 +1009,14 @@ public static string NonTphViewClash(object? entityType, object? otherEntityType public static string NoProviderConfigured => GetString("NoProviderConfigured"); + /// + /// Unable to modify a row in table '{table}' because its key column '{keyColumn}' is null. + /// + public static string NullKeyValue(object? table, object? keyColumn) + => string.Format( + GetString("NullKeyValue", nameof(table), nameof(keyColumn)), + table, keyColumn); + /// /// Expression '{sqlExpression}' in the SQL tree does not have a type mapping assigned. /// @@ -1192,7 +1208,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 800ae376254..f593fd008f0 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 differing 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}'). @@ -743,6 +746,9 @@ No relational database providers are configured. Configure a database provider using 'OnConfiguring' or by creating an ImmutableDbContextOptions with a configured database provider and passing it to the context. + + Unable to modify a row in table '{table}' because its key column '{keyColumn}' is null. + Expression '{sqlExpression}' in the SQL tree does not have a type mapping assigned. 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..00c7f278e84 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? _providerValueComparer; + /// /// 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 ProviderValueComparer + => NonCapturingLazyInitializer.EnsureInitialized( + ref _providerValueComparer, + 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/IReadOnlyModificationCommand.cs b/src/EFCore.Relational/Update/IReadOnlyModificationCommand.cs index ad125cfb000..43586af2dd8 100644 --- a/src/EFCore.Relational/Update/IReadOnlyModificationCommand.cs +++ b/src/EFCore.Relational/Update/IReadOnlyModificationCommand.cs @@ -17,6 +17,11 @@ namespace Microsoft.EntityFrameworkCore.Update; /// public interface IReadOnlyModificationCommand { + /// + /// The table containing the data to be modified. + /// + public ITable? Table { get; } + /// /// The name of the table containing the data to be modified. /// diff --git a/src/EFCore.Relational/Update/Internal/KeyValueIndexFactorySource.cs b/src/EFCore.Relational/Update/Internal/ColumnAccessors.cs similarity index 65% rename from src/EFCore.Relational/Update/Internal/KeyValueIndexFactorySource.cs rename to src/EFCore.Relational/Update/Internal/ColumnAccessors.cs index 4a554a30d1c..31f089d0859 100644 --- a/src/EFCore.Relational/Update/Internal/KeyValueIndexFactorySource.cs +++ b/src/EFCore.Relational/Update/Internal/ColumnAccessors.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,22 @@ 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 +// Sealed for perf +public sealed class ColumnAccessors { - 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); + public ColumnAccessors( + Delegate currentValueGetter, + Delegate originalValueGetter) + { + CurrentValueGetter = currentValueGetter; + OriginalValueGetter = originalValueGetter; + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -31,14 +32,13 @@ 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 })!; + public Delegate CurrentValueGetter { get; } - [UsedImplicitly] - private static IKeyValueIndexFactory CreateFactory(IKey key) - where TKey : notnull - => new KeyValueIndexFactory(key.GetPrincipalKeyValueFactory()); + /// + /// This is an internal API that supports the 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 Delegate OriginalValueGetter { get; } } diff --git a/src/EFCore.Relational/Update/Internal/ColumnAccessorsFactory.cs b/src/EFCore.Relational/Update/Internal/ColumnAccessorsFactory.cs new file mode 100644 index 00000000000..6cf9b492358 --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/ColumnAccessorsFactory.cs @@ -0,0 +1,117 @@ +// 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; + +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 static class ColumnAccessorsFactory +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static ColumnAccessors Create(IColumn column) + => (ColumnAccessors)GenericCreate + .MakeGenericMethod(column.ProviderClrType) + .Invoke(null, new object[] { column })!; + + private static readonly MethodInfo GenericCreate + = typeof(ColumnAccessorsFactory).GetTypeInfo().GetDeclaredMethod(nameof(CreateGeneric))!; + + [UsedImplicitly] + private static ColumnAccessors CreateGeneric(IColumn column) + => new( + CreateCurrentValueGetter(column), + CreateOriginalValueGetter(column)); + + private static Func CreateCurrentValueGetter(IColumn column) + => c => + { + if (c.Entries.Count > 0) + { + var value = default(TColumn)!; + var valueFound = false; + for (var i = 0; i < c.Entries.Count; i++) + { + var entry = c.Entries[i]; + var property = column.FindColumnMapping(entry.EntityType)?.Property; + if (property == null) + { + continue; + } + + var providerValue = entry.GetCurrentProviderValue(property); + if (providerValue == null + && !typeof(TColumn).IsNullableType()) + { + return (value!, valueFound); + } + + value = (TColumn)providerValue!; + valueFound = true; + if (entry.EntityState == EntityState.Added + || entry.IsModified(property)) + { + return (value, valueFound); + } + } + + return (value, valueFound); + } + + var modification = c.ColumnModifications.FirstOrDefault(m => m.ColumnName == column.Name); + return modification == null + ? (default(TColumn)!, false) + : ((TColumn)modification.Value!, true); + }; + + private static Func CreateOriginalValueGetter(IColumn column) + => c => + { + if (c.Entries.Count > 0) + { + var value = default(TColumn)!; + var valueFound = false; + for (var i = 0; i < c.Entries.Count; i++) + { + var entry = c.Entries[i]; + var property = column.FindColumnMapping(entry.EntityType)?.Property; + if (property == null) + { + continue; + } + + var providerValue = entry.GetOriginalProviderValue(property); + if (providerValue == null + && !typeof(TColumn).IsNullableType()) + { + return (value!, valueFound); + } + + value = (TColumn)providerValue!; + valueFound = true; + if (entry.EntityState == EntityState.Unchanged + || (entry.EntityState == EntityState.Modified && !entry.IsModified(property))) + { + return (value, valueFound); + } + } + + return (value, valueFound); + } + + var modification = c.ColumnModifications.FirstOrDefault(m => m.ColumnName == column.Name); + return modification == null + ? (default(TColumn)!, false) + : ((TColumn)modification.OriginalValue!, true); + }; +} diff --git a/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs b/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs index 28e73094e27..3e58ba607ac 100644 --- a/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs +++ b/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text; +using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace Microsoft.EntityFrameworkCore.Update.Internal; @@ -191,16 +192,16 @@ protected virtual IEnumerable CreateModificationCo command = sharedCommandsMap.GetOrAddValue( entry, - (n, s, comparer) => Dependencies.ModificationCommandFactory.CreateModificationCommand( + (t, comparer) => Dependencies.ModificationCommandFactory.CreateModificationCommand( new ModificationCommandParameters( - n, s, _sensitiveLoggingEnabled, comparer, generateParameterName, Dependencies.UpdateLogger))); + t, _sensitiveLoggingEnabled, comparer, generateParameterName, Dependencies.UpdateLogger))); isMainEntry = sharedCommandsMap.IsMainEntry(entry); } else { command = Dependencies.ModificationCommandFactory.CreateModificationCommand( new ModificationCommandParameters( - table.Name, table.Schema, _sensitiveLoggingEnabled, comparer: null, generateParameterName, + table, _sensitiveLoggingEnabled, comparer: null, generateParameterName, Dependencies.UpdateLogger)); } @@ -279,15 +280,13 @@ protected virtual IReadOnlyList> TopologicalS _modificationCommandGraph.Clear(); _modificationCommandGraph.AddVertices(commands); - // The predecessors map allows to populate the graph in linear time - var predecessorsMap = CreateKeyValuePredecessorMap(_modificationCommandGraph); - AddForeignKeyEdges(_modificationCommandGraph, predecessorsMap); + AddForeignKeyEdges(_modificationCommandGraph); AddUniqueValueEdges(_modificationCommandGraph); AddSameTableEdges(_modificationCommandGraph); - return _modificationCommandGraph.BatchingTopologicalSort(static (_, _, edges) => edges.All(e => e is IEntityType), FormatCycle); + return _modificationCommandGraph.BatchingTopologicalSort(static (_, _, edges) => edges.All(e => e is ITable), FormatCycle); } private string FormatCycle( @@ -301,12 +300,18 @@ private string FormatCycle( switch (annotatables.First()) { - case IForeignKey foreignKey: + case IForeignKeyConstraint foreignKey: Format(foreignKey, command1, command2, builder); break; - case IIndex index: + case IUniqueConstraint key: + Format(key, command1, command2, builder); + break; + case ITableIndex index: Format(index, command1, command2, builder); break; + default: + builder.AppendLine(" <-"); + break; } if (i == data.Count - 1) @@ -359,12 +364,12 @@ private void Format(IReadOnlyModificationCommand command, StringBuilder builder) } private void Format( - IForeignKey foreignKey, + IForeignKeyConstraint foreignKey, IReadOnlyModificationCommand source, IReadOnlyModificationCommand target, StringBuilder builder) { - var reverseDependency = !source.Entries.Any(e => foreignKey.DeclaringEntityType.IsAssignableFrom(e.EntityType)); + var reverseDependency = source.Table != foreignKey.Table; if (reverseDependency) { builder.AppendLine(" <-"); @@ -374,54 +379,38 @@ private void Format( builder.Append(' '); } - if (foreignKey.DependentToPrincipal != null - || foreignKey.PrincipalToDependent != null) - { - if (!reverseDependency - && foreignKey.DependentToPrincipal != null) - { - builder.Append(foreignKey.DependentToPrincipal.Name); - builder.Append(' '); - } + builder.Append("ForeignKey { "); - if (foreignKey.PrincipalToDependent != null) - { - builder.Append(foreignKey.PrincipalToDependent.Name); - builder.Append(' '); - } + var rowForeignKeyValueFactory = ((ForeignKeyConstraint)foreignKey).GetRowForeignKeyValueFactory(); + var dependentCommand = reverseDependency ? target : source; + var values = rowForeignKeyValueFactory.CreateDependentKeyValue(dependentCommand, fromOriginalValues: !reverseDependency)!; + FormatValues(values, foreignKey.Columns, dependentCommand, builder); - if (reverseDependency - && foreignKey.DependentToPrincipal != null) - { - builder.Append(foreignKey.DependentToPrincipal.Name); - builder.Append(' '); - } + builder.Append(" } "); + + if (!reverseDependency) + { + builder.AppendLine("<-"); + } + } + + private void Format(IUniqueConstraint key, IReadOnlyModificationCommand source, IReadOnlyModificationCommand target, StringBuilder builder) + { + var reverseDependency = source.EntityState != EntityState.Deleted; + if (reverseDependency) + { + builder.AppendLine(" <-"); } else { - builder.Append("ForeignKey "); + builder.Append(' '); } + builder.Append("Key { "); + var rowForeignKeyValueFactory = ((UniqueConstraint)key).GetRowKeyValueFactory(); var dependentCommand = reverseDependency ? target : source; - var dependentEntry = dependentCommand.Entries.First(e => foreignKey.DeclaringEntityType.IsAssignableFrom(e.EntityType)); - builder.Append("{ "); - for (var i = 0; i < foreignKey.Properties.Count; i++) - { - var property = foreignKey.Properties[i]; - builder.Append('\''); - builder.Append(property.Name); - builder.Append('\''); - if (_sensitiveLoggingEnabled) - { - builder.Append(": "); - builder.Append(dependentEntry.GetCurrentValue(property)); - } - - if (i != foreignKey.Properties.Count - 1) - { - builder.Append(", "); - } - } + var values = rowForeignKeyValueFactory.CreateKeyValue(dependentCommand, fromOriginalValues: !reverseDependency)!; + FormatValues(values, key.Columns, dependentCommand, builder); builder.Append(" } "); @@ -431,7 +420,7 @@ private void Format( } } - private void Format(IIndex index, IReadOnlyModificationCommand source, IReadOnlyModificationCommand target, StringBuilder builder) + private void Format(ITableIndex index, IReadOnlyModificationCommand source, IReadOnlyModificationCommand target, StringBuilder builder) { var reverseDependency = source.EntityState != EntityState.Deleted; if (reverseDependency) @@ -443,212 +432,184 @@ private void Format(IIndex index, IReadOnlyModificationCommand source, IReadOnly builder.Append(' '); } - builder.Append("Index "); + builder.Append("Index { "); + var rowForeignKeyValueFactory = ((TableIndex)index).GetRowIndexValueFactory(); var dependentCommand = reverseDependency ? target : source; - var dependentEntry = dependentCommand.Entries.First(e => index.DeclaringEntityType.IsAssignableFrom(e.EntityType)); - builder.Append("{ "); - for (var i = 0; i < index.Properties.Count; i++) + var values = rowForeignKeyValueFactory.CreateValue(dependentCommand, fromOriginalValues: !reverseDependency)!; + FormatValues(values, index.Columns, dependentCommand, builder); + + builder.Append(" } "); + + if (!reverseDependency) + { + builder.AppendLine("<-"); + } + } + + private void FormatValues(object[] values, IReadOnlyList columns, IReadOnlyModificationCommand dependentCommand, StringBuilder builder) + { + for (var i = 0; i < columns.Count; i++) { - var property = index.Properties[i]; + var column = columns[i]; builder.Append('\''); - builder.Append(property.Name); + builder.Append(column.Name); builder.Append('\''); if (_sensitiveLoggingEnabled) { builder.Append(": "); - builder.Append(dependentEntry.GetCurrentValue(property)); + builder.Append(values[i]); } - if (i != index.Properties.Count - 1) + if (i != columns.Count - 1) { builder.Append(", "); } } - - builder.Append(" } "); - - if (!reverseDependency) - { - builder.AppendLine("<-"); - } } - // Builds a map from foreign key values to list of modification commands, with an entry for every command - // that may need to precede some other command involving that foreign key value. - private Dictionary> CreateKeyValuePredecessorMap( + private void AddForeignKeyEdges( Multigraph commandGraph) { - var predecessorsMap = new Dictionary>(); + var predecessorsMap = new Dictionary>(); + var originalPredecessorsMap = new Dictionary>(); foreach (var command in commandGraph.Vertices) { - if (command.EntityState == EntityState.Modified - || command.EntityState == EntityState.Added) + if (command.EntityState is EntityState.Modified or EntityState.Added) { - // ReSharper disable once ForCanBeConvertedToForeach - for (var i = 0; i < command.Entries.Count; i++) + foreach (var foreignKey in command.Table!.ReferencingForeignKeyConstraints) { - var entry = command.Entries[i]; - foreach (var foreignKey in entry.EntityType.GetReferencingForeignKeys()) + if (!IsModified(foreignKey.PrincipalUniqueConstraint.Columns, command)) { - if (!IsMapped(foreignKey, command, principal: true) - || !IsModified(foreignKey.PrincipalKey.Properties, entry)) - { - continue; - } + continue; + } - var principalKeyValue = Dependencies.KeyValueIndexFactorySource - .GetKeyValueIndexFactory(foreignKey.PrincipalKey) - .CreatePrincipalKeyValue(entry, foreignKey); + var principalKeyValue = ((ForeignKeyConstraint)foreignKey).GetRowForeignKeyValueFactory() + .CreatePrincipalValueIndex(command); + Check.DebugAssert(principalKeyValue != null, "null principalKeyValue"); - if (principalKeyValue != null) - { - if (!predecessorsMap.TryGetValue(principalKeyValue, out var predecessorCommands)) - { - predecessorCommands = new List(); - predecessorsMap.Add(principalKeyValue, predecessorCommands); - } - - predecessorCommands.Add(command); - } + if (!predecessorsMap.TryGetValue(principalKeyValue, out var predecessorCommands)) + { + predecessorCommands = new List(); + predecessorsMap.Add(principalKeyValue, predecessorCommands); } + + predecessorCommands.Add(command); } } - if (command.EntityState == EntityState.Modified - || command.EntityState == EntityState.Deleted) + if (command.EntityState is EntityState.Modified or EntityState.Deleted) { - foreach (var entry in command.Entries) + foreach (var foreignKey in command.Table!.ForeignKeyConstraints) { - foreach (var foreignKey in entry.EntityType.GetForeignKeys()) + if (!IsModified(foreignKey.Columns, command)) { - if (!IsMapped(foreignKey, command, principal: false) - || !IsModified(foreignKey.Properties, entry)) - { - continue; - } - - var dependentKeyValue = Dependencies.KeyValueIndexFactorySource - .GetKeyValueIndexFactory(foreignKey.PrincipalKey) - .CreateDependentKeyValueFromOriginalValues(entry, foreignKey); + continue; + } - if (dependentKeyValue != null) + var dependentKeyValue = ((ForeignKeyConstraint)foreignKey).GetRowForeignKeyValueFactory() + .CreateDependentValueIndex(command, fromOriginalValues: true); + if (dependentKeyValue != null) + { + if (!originalPredecessorsMap.TryGetValue(dependentKeyValue, out var predecessorCommands)) { - if (!predecessorsMap.TryGetValue(dependentKeyValue, out var predecessorCommands)) - { - predecessorCommands = new List(); - predecessorsMap.Add(dependentKeyValue, predecessorCommands); - } - - predecessorCommands.Add(command); + predecessorCommands = new(); + originalPredecessorsMap.Add(dependentKeyValue, predecessorCommands); } + + predecessorCommands.Add(command); } } } } - return predecessorsMap; - } - - private void AddForeignKeyEdges( - Multigraph commandGraph, - Dictionary> predecessorsMap) - { foreach (var command in commandGraph.Vertices) { - switch (command.EntityState) + if (command.EntityState is EntityState.Modified or EntityState.Added) { - case EntityState.Modified: - case EntityState.Added: - // ReSharper disable once ForCanBeConvertedToForeach - for (var entryIndex = 0; entryIndex < command.Entries.Count; entryIndex++) + foreach (var foreignKey in command.Table!.ForeignKeyConstraints) + { + if (!IsModified(foreignKey.Columns, command)) { - var entry = command.Entries[entryIndex]; - foreach (var foreignKey in entry.EntityType.GetForeignKeys()) - { - if (!IsMapped(foreignKey, command, principal: false) - || !IsModified(foreignKey.Properties, entry)) - { - continue; - } - - var dependentKeyValue = Dependencies.KeyValueIndexFactorySource - .GetKeyValueIndexFactory(foreignKey.PrincipalKey) - .CreateDependentKeyValue(entry, foreignKey); - if (dependentKeyValue == null) - { - continue; - } - - AddMatchingPredecessorEdge( - predecessorsMap, dependentKeyValue, commandGraph, command, foreignKey); - } + continue; } - break; - case EntityState.Deleted: - // ReSharper disable once ForCanBeConvertedToForeach - for (var entryIndex = 0; entryIndex < command.Entries.Count; entryIndex++) + var dependentKeyValue = ((ForeignKeyConstraint)foreignKey).GetRowForeignKeyValueFactory() + .CreateDependentValueIndex(command); + if (dependentKeyValue != null) { - var entry = command.Entries[entryIndex]; - foreach (var foreignKey in entry.EntityType.GetReferencingForeignKeys()) - { - if (!IsMapped(foreignKey, command, principal: true)) - { - continue; - } - - var principalKeyValue = Dependencies.KeyValueIndexFactorySource - .GetKeyValueIndexFactory(foreignKey.PrincipalKey) - .CreatePrincipalKeyValueFromOriginalValues(entry, foreignKey); - if (principalKeyValue != null) - { - AddMatchingPredecessorEdge( - predecessorsMap, principalKeyValue, commandGraph, command, foreignKey); - } - } + AddMatchingPredecessorEdge( + predecessorsMap, dependentKeyValue, commandGraph, command, foreignKey); } - - break; - } - } - } - - private static bool IsMapped(IForeignKey foreignKey, IReadOnlyModificationCommand command, bool principal) - { - foreach (var constraint in foreignKey.GetMappedConstraints()) - { - if (principal) - { - if (constraint.PrincipalTable.Name == command.TableName - && constraint.PrincipalTable.Schema == command.Schema) - { - return true; } } - else + + if (command.EntityState is EntityState.Modified or EntityState.Deleted) { - if (constraint.Table.Name == command.TableName - && constraint.Table.Schema == command.Schema) + foreach (var foreignKey in command.Table!.ReferencingForeignKeyConstraints) { - return true; + if (!IsModified(foreignKey.PrincipalUniqueConstraint.Columns, command)) + { + continue; + } + + var principalKeyValue = ((ForeignKeyConstraint)foreignKey).GetRowForeignKeyValueFactory() + .CreatePrincipalValueIndex(command, fromOriginalValues: true); + Check.DebugAssert(principalKeyValue != null, "null principalKeyValue"); + AddMatchingPredecessorEdge( + originalPredecessorsMap, principalKeyValue, commandGraph, command, foreignKey); } } } - - return false; } - private static bool IsModified(IReadOnlyList properties, IUpdateEntry entry) + private static bool IsModified(IReadOnlyList columns, IReadOnlyModificationCommand command) { - if (entry.EntityState != EntityState.Modified) + if (command.EntityState != EntityState.Modified) { return true; } - foreach (var property in properties) + for (var columnIndex = 0; columnIndex < columns.Count; columnIndex++) { - if (entry.IsModified(property)) + var column = columns[columnIndex]; + object? originalValue = null; + object? currentValue = null; + RelationalTypeMapping? typeMapping = null; + for (var entryIndex = 0; entryIndex < command.Entries.Count; entryIndex++) + { + var entry = command.Entries[entryIndex]; + var columnMapping = column.FindColumnMapping(entry.EntityType); + var property = columnMapping?.Property; + if (property != null + && ((property.GetAfterSaveBehavior() == PropertySaveBehavior.Save) + || (!property.IsPrimaryKey() && entry.EntityState != EntityState.Modified))) + { + switch (entry.EntityState) + { + case EntityState.Added: + currentValue = entry.GetCurrentProviderValue(property); + break; + case EntityState.Deleted: + case EntityState.Unchanged: + originalValue ??= entry.GetOriginalProviderValue(property); + break; + case EntityState.Modified: + if (entry.IsModified(property)) + { + return true; + } + + originalValue ??= entry.GetOriginalProviderValue(property); + break; + } + + typeMapping = columnMapping!.TypeMapping; + } + } + + if (typeMapping != null + && !typeMapping.ProviderValueComparer.Equals(originalValue, currentValue)) { return true; } @@ -679,72 +640,55 @@ private static void AddMatchingPredecessorEdge( private void AddUniqueValueEdges(Multigraph commandGraph) { - Dictionary>? indexPredecessorsMap = null; - var keyPredecessorsMap = new Dictionary<(IKey, IKeyValueIndex), List>(); + Dictionary>? indexPredecessorsMap = null; + var keyPredecessorsMap = new Dictionary>(); foreach (var command in commandGraph.Vertices) { - if (command.EntityState != EntityState.Modified - && command.EntityState != EntityState.Deleted) + if (command.EntityState is EntityState.Added) { continue; } - for (var entryIndex = 0; entryIndex < command.Entries.Count; entryIndex++) + foreach (var index in command.Table!.Indexes) { - var entry = command.Entries[entryIndex]; - foreach (var index in entry.EntityType.GetIndexes()) - { - if (!index.IsUnique - || !index.GetMappedTableIndexes().Any() - || !IsModified(index.Properties, entry)) - { - continue; - } - - var valueFactory = index.GetNullableValueFactory(); - if (valueFactory.TryCreateFromOriginalValues(entry, out var indexValue)) - { - indexPredecessorsMap ??= new Dictionary>(); - if (!indexPredecessorsMap.TryGetValue(index, out var predecessorCommands)) - { - predecessorCommands = new Dictionary(valueFactory.EqualityComparer); - indexPredecessorsMap.Add(index, predecessorCommands); - } - - if (!predecessorCommands.ContainsKey(indexValue)) - { - predecessorCommands.Add(indexValue, command); - } - } - } - - if (command.EntityState != EntityState.Deleted) + if (!index.IsUnique + || !IsModified(index.Columns, command)) { continue; } - foreach (var key in entry.EntityType.GetKeys()) + var indexValue = ((TableIndex)index).GetRowIndexValueFactory() + .CreateValueIndex(command, fromOriginalValues: true); + if (indexValue != null) { - if (!key.GetMappedConstraints().Any()) + indexPredecessorsMap ??= new(); + if (!indexPredecessorsMap.TryGetValue(indexValue, out var predecessorCommands)) { - continue; + predecessorCommands = new(); + indexPredecessorsMap.Add(indexValue, predecessorCommands); } - var principalKeyValue = Dependencies.KeyValueIndexFactorySource - .GetKeyValueIndexFactory(key) - .CreatePrincipalKeyValue(entry, null); + predecessorCommands.Add(command); + } + } - if (principalKeyValue != null) - { - if (!keyPredecessorsMap.TryGetValue((key, principalKeyValue), out var predecessorCommands)) - { - predecessorCommands = new List(); - keyPredecessorsMap.Add((key, principalKeyValue), predecessorCommands); - } + if (command.EntityState is not EntityState.Deleted) + { + continue; + } - predecessorCommands.Add(command); - } + foreach (var key in command.Table.UniqueConstraints) + { + var keyValue = ((UniqueConstraint)key).GetRowKeyValueFactory() + .CreateValueIndex(command, fromOriginalValues: true); + Check.DebugAssert(keyValue != null, "null keyValue"); + if (!keyPredecessorsMap.TryGetValue((key, keyValue), out var predecessorCommands)) + { + predecessorCommands = new List(); + keyPredecessorsMap.Add((key, keyValue), predecessorCommands); } + + predecessorCommands.Add(command); } } @@ -752,30 +696,25 @@ private void AddUniqueValueEdges(Multigraph(); - if (valueFactory.TryCreateFromCurrentValues(entry, out var indexValue) - && indexPredecessorsMap.TryGetValue(index, out var predecessorCommands) - && predecessorCommands.TryGetValue(indexValue, out var predecessor) - && predecessor != command) - { - commandGraph.AddEdge(predecessor, command, index); - } + var indexValue = ((TableIndex)index).GetRowIndexValueFactory() + .CreateValueIndex(command); + if (indexValue != null) + { + AddMatchingPredecessorEdge( + indexPredecessorsMap, indexValue, commandGraph, command, index); } } } @@ -785,30 +724,19 @@ private void AddUniqueValueEdges(Multigraph 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/KeyValueIndexFactory.cs b/src/EFCore.Relational/Update/Internal/CompositeRowForeignKeyValueFactory.cs similarity index 64% rename from src/EFCore.Relational/Update/Internal/KeyValueIndexFactory.cs rename to src/EFCore.Relational/Update/Internal/CompositeRowForeignKeyValueFactory.cs index c46fa8d4d4e..96d39e55aa1 100644 --- a/src/EFCore.Relational/Update/Internal/KeyValueIndexFactory.cs +++ b/src/EFCore.Relational/Update/Internal/CompositeRowForeignKeyValueFactory.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.Metadata.Internal; + namespace Microsoft.EntityFrameworkCore.Update.Internal; /// @@ -9,9 +11,10 @@ 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 class CompositeRowForeignKeyValueFactory : CompositeRowValueFactory, IRowForeignKeyValueFactory { - private readonly IPrincipalKeyValueFactory _principalKeyValueFactory; + private readonly IForeignKeyConstraint _foreignKey; + private readonly IRowKeyValueFactory _principalKeyValueFactory; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -19,9 +22,11 @@ public class KeyValueIndexFactory : 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. /// - public KeyValueIndexFactory(IPrincipalKeyValueFactory principalKeyValueFactory) + public CompositeRowForeignKeyValueFactory(IForeignKeyConstraint foreignKey) + : base(foreignKey.Columns) { - _principalKeyValueFactory = principalKeyValueFactory; + _foreignKey = foreignKey; + _principalKeyValueFactory = (IRowKeyValueFactory)((UniqueConstraint)foreignKey.PrincipalUniqueConstraint).GetRowKeyValueFactory(); } /// @@ -30,12 +35,11 @@ public KeyValueIndexFactory(IPrincipalKeyValueFactory principalKeyValueFac /// any release. You should only use it directly in 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); + public virtual object CreatePrincipalValueIndex(IReadOnlyModificationCommand command, bool fromOriginalValues = false) + => new ValueIndex( + _foreignKey, + _principalKeyValueFactory.CreateKeyValue(command, fromOriginalValues), + EqualityComparer); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -43,12 +47,10 @@ 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); + public virtual object? CreateDependentValueIndex(IReadOnlyModificationCommand command, bool fromOriginalValues = false) + => TryCreateDependentKeyValue(command, fromOriginalValues, out var keyValue) + ? new ValueIndex(_foreignKey, keyValue, EqualityComparer) + : null; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -56,10 +58,8 @@ 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; + public virtual object[] CreatePrincipalKeyValue(IReadOnlyModificationCommand command, bool fromOriginalValues = false) + => _principalKeyValueFactory.CreateKeyValue(command, fromOriginalValues)!; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -67,8 +67,8 @@ 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) + public virtual object[]? CreateDependentKeyValue(IReadOnlyModificationCommand command, bool fromOriginalValues = false) + => TryCreateDependentKeyValue(command, fromOriginalValues, out var keyValue) + ? (object[])keyValue : null; } diff --git a/src/EFCore.Relational/Update/Internal/CompositeRowIndexValueFactory.cs b/src/EFCore.Relational/Update/Internal/CompositeRowIndexValueFactory.cs new file mode 100644 index 00000000000..e1e5a3a35aa --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/CompositeRowIndexValueFactory.cs @@ -0,0 +1,78 @@ +// 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 CompositeRowIndexValueFactory : CompositeRowValueFactory, IRowIndexValueFactory +{ + private readonly ITableIndex _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 CompositeRowIndexValueFactory(ITableIndex index) + : base(index.Columns) + { + _index = 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 bool TryCreateIndexValue(object?[] keyValues, [NotNullWhen(true)] out object?[]? key) + => TryCreateDependentKeyValue(keyValues, 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 TryCreateIndexValue(IDictionary keyValues, [NotNullWhen(true)] out object?[]? key) + => TryCreateDependentKeyValue(keyValues, 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 TryCreateIndexValue(IReadOnlyModificationCommand command, bool fromOriginalValues, [NotNullWhen(true)] out object?[]? key) + => TryCreateDependentKeyValue(command, fromOriginalValues, 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 object? CreateValueIndex(IReadOnlyModificationCommand command, bool fromOriginalValues = false) + => TryCreateDependentKeyValue(command, fromOriginalValues, out var keyValue) + ? new ValueIndex(_index, keyValue, EqualityComparer) + : 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[]? CreateValue(IReadOnlyModificationCommand command, bool fromOriginalValues = false) + => TryCreateIndexValue(command, fromOriginalValues, out var keyValue) + ? (object[])keyValue + : null; +} diff --git a/src/EFCore.Relational/Update/Internal/CompositeRowKeyValueFactory.cs b/src/EFCore.Relational/Update/Internal/CompositeRowKeyValueFactory.cs new file mode 100644 index 00000000000..212c96b215f --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/CompositeRowKeyValueFactory.cs @@ -0,0 +1,123 @@ +// 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 class CompositeRowKeyValueFactory : CompositeRowValueFactory, IRowKeyValueFactory +{ + private readonly IUniqueConstraint _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 CompositeRowKeyValueFactory(IUniqueConstraint key) + : base(key.Columns) + { + _constraint = 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 object?[] CreateKeyValue(object?[] keyValues) + { + if (keyValues.Any(v => v == null)) + { + throw new InvalidOperationException( + RelationalStrings.NullKeyValue( + _constraint.Table.SchemaQualifiedName, + FindNullColumnInKeyValues(keyValues).Name)); + } + + return 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?[] CreateKeyValue(IDictionary keyValues) + { + if (!TryCreateDependentKeyValue(keyValues, out var key)) + { + throw new InvalidOperationException( + RelationalStrings.NullKeyValue( + _constraint.Table.SchemaQualifiedName, + FindNullColumnInKeyValues(key).Name)); + } + + return 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 object?[] CreateKeyValue(IReadOnlyModificationCommand command, bool fromOriginalValues = false) + { + if (!TryCreateDependentKeyValue(command, fromOriginalValues, out var key)) + { + throw new InvalidOperationException( + RelationalStrings.NullKeyValue( + _constraint.Table.SchemaQualifiedName, + FindNullColumnInKeyValues(key).Name)); + } + + return key; + } + + private IColumn FindNullColumnInKeyValues(object?[]? keyValues) + { + var index = 0; + if (keyValues != null) + { + for (var i = 0; i < keyValues.Length; i++) + { + if (keyValues[i] == null) + { + index = i; + break; + } + } + } + + return _constraint.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 CreateValueIndex(IReadOnlyModificationCommand command, bool fromOriginalValues = false) + => new ValueIndex( + _constraint, + CreateKeyValue(command, fromOriginalValues), + 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. + /// + object[] IRowKeyValueFactory.CreateKeyValue(IReadOnlyModificationCommand command, bool fromOriginalValues) + => CreateKeyValue(command, fromOriginalValues)!; +} diff --git a/src/EFCore.Relational/Update/Internal/CompositeRowValueFactory.cs b/src/EFCore.Relational/Update/Internal/CompositeRowValueFactory.cs new file mode 100644 index 00000000000..348495ac8ad --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/CompositeRowValueFactory.cs @@ -0,0 +1,215 @@ +// 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; +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 abstract class CompositeRowValueFactory +{ + /// + /// This is an internal API that supports the 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 TryCreateDependentKeyValue(object?[] keyValues, [NotNullWhen(true)] out object?[]? key) + { + key = keyValues; + return keyValues.All(k => k != 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 TryCreateDependentKeyValue(IDictionary keyValues, [NotNullWhen(true)] out object?[]? key) + { + key = new object[Columns.Count]; + var index = 0; + + foreach (var column in Columns) + { + if (!keyValues.TryGetValue(column.Name, out var value) + || value == 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 TryCreateDependentKeyValue(IReadOnlyModificationCommand command, bool fromOriginalValues, [NotNullWhen(true)] out object?[]? key) + { + key = new object[Columns.Count]; + var index = 0; + + for (var i = 0; i < Columns.Count; i++) + { + var column = Columns[i]; + + if (command.Entries.Count > 0) + { + object? value = null; + var valueFound = false; + foreach (var entry in command.Entries) + { + var property = column.FindColumnMapping(entry.EntityType)?.Property; + if (property == null) + { + continue; + } + + valueFound = true; + value = fromOriginalValues ? entry.GetOriginalProviderValue(property) : entry.GetCurrentProviderValue(property); + if (!fromOriginalValues + && (entry.EntityState == EntityState.Added + || (entry.EntityState == EntityState.Modified && entry.IsModified(property)))) + { + break; + } + + if (fromOriginalValues + && entry.EntityState != EntityState.Added) + { + break; + } + } + + if (!valueFound) + { + return false; + } + key[index++] = value; + } + else + { + var modification = command.ColumnModifications.FirstOrDefault(m => m.ColumnName == column.Name); + if (modification == null) + { + return false; + } + + var value = fromOriginalValues ? modification.OriginalValue : modification.Value; + 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 columns) + => new CompositeCustomComparer(columns.Select(c => c.PropertyMappings.First().TypeMapping.ProviderValueComparer).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++) + { + var value = obj[i]; + var hash = value == null ? 0 : _hashCodes[i](value); + hashCode = (hashCode * 397) ^ hash; + } + + return hashCode; + } + } +} diff --git a/src/EFCore.Relational/Update/Internal/IRowForeignKeyValueFactory.cs b/src/EFCore.Relational/Update/Internal/IRowForeignKeyValueFactory.cs new file mode 100644 index 00000000000..f0842b1da36 --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/IRowForeignKeyValueFactory.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 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. + /// + object[] CreatePrincipalKeyValue(IReadOnlyModificationCommand 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. + /// + object[]? CreateDependentKeyValue(IReadOnlyModificationCommand 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. + /// + object CreatePrincipalValueIndex(IReadOnlyModificationCommand 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. + /// + object? CreateDependentValueIndex(IReadOnlyModificationCommand 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/KeyValueIndex.cs b/src/EFCore.Relational/Update/Internal/IRowForeignKeyValueFactory`.cs similarity index 64% rename from src/EFCore.Relational/Update/Internal/KeyValueIndex.cs rename to src/EFCore.Relational/Update/Internal/IRowForeignKeyValueFactory`.cs index 079208fca14..b53c488df6f 100644 --- a/src/EFCore.Relational/Update/Internal/KeyValueIndex.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,30 +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 sealed class KeyValueIndex : IKeyValueIndex +public interface IRowForeignKeyValueFactory : IRowForeignKeyValueFactory { - private readonly IForeignKey? _foreignKey; - 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 /// 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 KeyValueIndex( - IForeignKey? foreignKey, - TKey keyValue, - IEqualityComparer keyComparer, - bool fromOriginalValues) - { - _foreignKey = foreignKey; - _keyValue = keyValue; - _fromOriginalValues = fromOriginalValues; - _keyComparer = keyComparer; - } + 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 @@ -40,13 +27,7 @@ public KeyValueIndex( /// any release. You should only use it directly in 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 - && _keyComparer.Equals(_keyValue, other._keyValue); + bool TryCreateDependentKeyValue(IDictionary keyValues, [NotNullWhen(true)] out TKey? key); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -54,11 +35,7 @@ private bool Equals(KeyValueIndex other) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public override bool Equals(object? obj) - => !(obj is null) - && (ReferenceEquals(this, obj) - || obj.GetType() == GetType() - && Equals((KeyValueIndex)obj)); + bool TryCreateDependentKeyValue(IReadOnlyModificationCommand command, bool fromOriginalValues, [NotNullWhen(true)] out TKey? key); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -66,13 +43,5 @@ public override bool Equals(object? obj) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public override int GetHashCode() - { - var hash = new HashCode(); - hash.Add(typeof(TKey)); - hash.Add(_fromOriginalValues); - hash.Add(_foreignKey); - hash.Add(_keyValue, _keyComparer); - return hash.ToHashCode(); - } + IEqualityComparer EqualityComparer { get; } } diff --git a/src/EFCore.Relational/Update/Internal/IRowIndexValueFactory.cs b/src/EFCore.Relational/Update/Internal/IRowIndexValueFactory.cs new file mode 100644 index 00000000000..af90ff203fa --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/IRowIndexValueFactory.cs @@ -0,0 +1,29 @@ +// 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 IRowIndexValueFactory +{ + /// + /// This is an internal API that supports the 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[]? CreateValue(IReadOnlyModificationCommand 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. + /// + object? CreateValueIndex(IReadOnlyModificationCommand command, bool fromOriginalValues = false); +} diff --git a/src/EFCore.Relational/Update/Internal/IKeyValueIndex.cs b/src/EFCore.Relational/Update/Internal/IRowIndexValueFactoryFactory.cs similarity index 91% rename from src/EFCore.Relational/Update/Internal/IKeyValueIndex.cs rename to src/EFCore.Relational/Update/Internal/IRowIndexValueFactoryFactory.cs index 4005109ef2e..9aefff34bf4 100644 --- a/src/EFCore.Relational/Update/Internal/IKeyValueIndex.cs +++ b/src/EFCore.Relational/Update/Internal/IRowIndexValueFactoryFactory.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 IRowIndexValueFactoryFactory { /// /// 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(); + IRowIndexValueFactory Create(ITableIndex index); } diff --git a/src/EFCore.Relational/Update/Internal/IRowIndexValueFactory`.cs b/src/EFCore.Relational/Update/Internal/IRowIndexValueFactory`.cs new file mode 100644 index 00000000000..a7033a3d2c0 --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/IRowIndexValueFactory`.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.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 IRowIndexValueFactory : IRowIndexValueFactory +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + bool TryCreateIndexValue(object?[] keyValues, [NotNullWhen(true)] out TKey? 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. + /// + bool TryCreateIndexValue(IDictionary keyValues, [NotNullWhen(true)] out TKey? 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. + /// + bool TryCreateIndexValue(IReadOnlyModificationCommand command, bool fromOriginalValues, [NotNullWhen(true)] out TKey? 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. + /// + IEqualityComparer EqualityComparer { get; } +} diff --git a/src/EFCore.Relational/Update/Internal/IRowKeyValueFactory.cs b/src/EFCore.Relational/Update/Internal/IRowKeyValueFactory.cs new file mode 100644 index 00000000000..de7106dc77e --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/IRowKeyValueFactory.cs @@ -0,0 +1,29 @@ +// 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[] CreateKeyValue(IReadOnlyModificationCommand 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. + /// + object CreateValueIndex(IReadOnlyModificationCommand command, bool fromOriginalValues = false); +} diff --git a/src/EFCore.Relational/Update/Internal/IRowKeyValueFactoryFactory.cs b/src/EFCore.Relational/Update/Internal/IRowKeyValueFactoryFactory.cs new file mode 100644 index 00000000000..c3c27cf9c63 --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/IRowKeyValueFactoryFactory.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 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. + /// + IRowKeyValueFactory Create(IUniqueConstraint key); +} diff --git a/src/EFCore.Relational/Update/Internal/IKeyValueIndexFactory.cs b/src/EFCore.Relational/Update/Internal/IRowKeyValueFactory`.cs similarity index 85% rename from src/EFCore.Relational/Update/Internal/IKeyValueIndexFactory.cs rename to src/EFCore.Relational/Update/Internal/IRowKeyValueFactory`.cs index cf87907b336..3236136d762 100644 --- a/src/EFCore.Relational/Update/Internal/IKeyValueIndexFactory.cs +++ b/src/EFCore.Relational/Update/Internal/IRowKeyValueFactory`.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 IRowKeyValueFactory : IRowKeyValueFactory { /// /// 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); + TKey CreateKeyValue(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); + TKey CreateKeyValue(IDictionary keyValues); /// /// 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); + new TKey CreateKeyValue(IReadOnlyModificationCommand command, bool fromOriginalValues = false); /// /// 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); + IEqualityComparer EqualityComparer { get; } } diff --git a/src/EFCore.Relational/Update/Internal/RowForeignKeyValueFactory.cs b/src/EFCore.Relational/Update/Internal/RowForeignKeyValueFactory.cs new file mode 100644 index 00000000000..fdec90c5e4e --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/RowForeignKeyValueFactory.cs @@ -0,0 +1,122 @@ +// 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; +using Microsoft.EntityFrameworkCore.Internal; +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 abstract class RowForeignKeyValueFactory : IRowForeignKeyValueFactory +{ + private readonly IForeignKeyConstraint _foreignKey; + 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 RowForeignKeyValueFactory(IForeignKeyConstraint foreignKey) + { + _foreignKey = foreignKey; + _principalKeyValueFactory = (IRowKeyValueFactory)((UniqueConstraint)foreignKey.PrincipalUniqueConstraint).GetRowKeyValueFactory(); + } + + /// + /// This is an internal API that supports the 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 abstract 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 object CreatePrincipalValueIndex(IReadOnlyModificationCommand command, bool fromOriginalValues = false) + => new ValueIndex( + _foreignKey, + _principalKeyValueFactory.CreateKeyValue(command, fromOriginalValues), + 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 object? CreateDependentValueIndex(IReadOnlyModificationCommand command, bool fromOriginalValues = false) + => TryCreateDependentKeyValue(command, fromOriginalValues, out var keyValue) + ? new ValueIndex(_foreignKey, keyValue, EqualityComparer) + : 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 abstract 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 + /// 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 abstract bool TryCreateDependentKeyValue( + IDictionary keyPropertyValues, [NotNullWhen(true)] out TKey? 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 abstract bool TryCreateDependentKeyValue( + IReadOnlyModificationCommand command, bool fromOriginalValues, [NotNullWhen(true)] out TKey? 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 IEqualityComparer CreateKeyEqualityComparer(IColumn column) +#pragma warning disable EF1001 // Internal EF Core API usage. + => NullableComparerAdapter.Wrap( + column.PropertyMappings.First().TypeMapping.ProviderValueComparer); +#pragma warning restore EF1001 // Internal EF Core API usage. + + /// + /// This is an internal API that supports the 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[] CreatePrincipalKeyValue(IReadOnlyModificationCommand command, bool fromOriginalValues = false) + => new object[] { _principalKeyValueFactory.CreateKeyValue(command, 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[]? CreateDependentKeyValue(IReadOnlyModificationCommand command, bool fromOriginalValues = false) + => TryCreateDependentKeyValue(command, fromOriginalValues, out var value) + ? (new object[] { value }) + : null; +} diff --git a/src/EFCore.Relational/Update/Internal/RowForeignKeyValueFactoryFactory.cs b/src/EFCore.Relational/Update/Internal/RowForeignKeyValueFactoryFactory.cs new file mode 100644 index 00000000000..c8d1f24f395 --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/RowForeignKeyValueFactoryFactory.cs @@ -0,0 +1,61 @@ +// 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) + => foreignKey.Columns.Count == 1 + ? (IRowForeignKeyValueFactory)_createMethod + .MakeGenericMethod(foreignKey.Columns.First().ProviderClrType) + .Invoke(null, new object[] { foreignKey })! + : new CompositeRowForeignKeyValueFactory(foreignKey); + + private readonly static MethodInfo _createMethod = typeof(RowForeignKeyValueFactoryFactory).GetTypeInfo() + .GetDeclaredMethod(nameof(CreateSimple))!; + + [UsedImplicitly] + private static IRowForeignKeyValueFactory CreateSimple(IForeignKeyConstraint foreignKey) + where TKey : notnull + { + var dependentColumn = foreignKey.Columns.Single(); + var dependentType = dependentColumn.ProviderClrType; + var principalType = foreignKey.PrincipalColumns.Single().ProviderClrType; + var columnAccessors = ((Column)dependentColumn).Accessors; + + if (dependentType.IsNullableType() + && principalType.IsNullableType()) + { + return new SimpleFullyNullableRowForeignKeyValueFactory(foreignKey, dependentColumn, columnAccessors); + } + + if (dependentType.IsNullableType()) + { + return (IRowForeignKeyValueFactory)Activator.CreateInstance( + typeof(SimpleNullableRowForeignKeyValueFactory<>).MakeGenericType( + typeof(TKey)), foreignKey, dependentColumn, columnAccessors)!; + } + + return principalType.IsNullableType() + ? (IRowForeignKeyValueFactory)Activator.CreateInstance( + typeof(SimpleNullablePrincipalRowForeignKeyValueFactory<,>).MakeGenericType( + typeof(TKey), typeof(TKey).UnwrapNullableType()), foreignKey, dependentColumn, columnAccessors)! + : new SimpleNonNullableRowForeignKeyValueFactory(foreignKey, dependentColumn, columnAccessors); + } +} diff --git a/src/EFCore.Relational/Update/Internal/RowIndexValueFactoryFactory.cs b/src/EFCore.Relational/Update/Internal/RowIndexValueFactoryFactory.cs new file mode 100644 index 00000000000..d907a33e3cd --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/RowIndexValueFactoryFactory.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; + +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 RowIndexValueFactoryFactory : IRowIndexValueFactoryFactory +{ + /// + /// This is an internal API that supports the 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 IRowIndexValueFactory Create(ITableIndex index) + => index.Columns.Count == 1 + ? (IRowIndexValueFactory)_createMethod + .MakeGenericMethod(index.Columns.First().ProviderClrType) + .Invoke(null, new object[] { index })! + : new CompositeRowIndexValueFactory(index); + + private static readonly MethodInfo _createMethod = typeof(RowIndexValueFactoryFactory).GetTypeInfo() + .GetDeclaredMethod(nameof(CreateSimple))!; + + [UsedImplicitly] + private static IRowIndexValueFactory CreateSimple(ITableIndex index) + => new SimpleRowIndexValueFactory(index); +} diff --git a/src/EFCore.Relational/Update/Internal/RowKeyValueFactoryFactory.cs b/src/EFCore.Relational/Update/Internal/RowKeyValueFactoryFactory.cs new file mode 100644 index 00000000000..4bed491fb95 --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/RowKeyValueFactoryFactory.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; + +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) + => key.Columns.Count == 1 + ? (IRowKeyValueFactory)_createMethod + .MakeGenericMethod(key.Columns.First().ProviderClrType) + .Invoke(null, new object[] { key })! + : new CompositeRowKeyValueFactory(key); + + private readonly static MethodInfo _createMethod = typeof(RowKeyValueFactoryFactory).GetTypeInfo() + .GetDeclaredMethod(nameof(CreateSimpleFactory))!; + + [UsedImplicitly] + private static IRowKeyValueFactory CreateSimpleFactory(IUniqueConstraint key) + => new SimpleRowKeyValueFactory(key); +} diff --git a/src/EFCore.Relational/Update/Internal/SharedTableEntryMap.cs b/src/EFCore.Relational/Update/Internal/SharedTableEntryMap.cs index 18a9ce31f05..fc723149ac7 100644 --- a/src/EFCore.Relational/Update/Internal/SharedTableEntryMap.cs +++ b/src/EFCore.Relational/Update/Internal/SharedTableEntryMap.cs @@ -54,7 +54,7 @@ public virtual TValue GetOrAddValue(IUpdateEntry entry, SharedTableEntryValueFac return sharedCommand; } - sharedCommand = createElement(_table.Name, _table.Schema, _comparer); + sharedCommand = createElement(_table, _comparer); _entryValueMap.Add(mainEntry, sharedCommand); return sharedCommand; diff --git a/src/EFCore.Relational/Update/Internal/SharedTableEntryValueFactory.cs b/src/EFCore.Relational/Update/Internal/SharedTableEntryValueFactory.cs index 14e5f1720bd..4196f65171d 100644 --- a/src/EFCore.Relational/Update/Internal/SharedTableEntryValueFactory.cs +++ b/src/EFCore.Relational/Update/Internal/SharedTableEntryValueFactory.cs @@ -9,4 +9,4 @@ 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 delegate TValue SharedTableEntryValueFactory(string tableName, string? schema, IComparer comparer); +public delegate TValue SharedTableEntryValueFactory(ITable table, IComparer comparer); diff --git a/src/EFCore.Relational/Update/Internal/SimpleFullyNullableRowForeignKeyValueFactory.cs b/src/EFCore.Relational/Update/Internal/SimpleFullyNullableRowForeignKeyValueFactory.cs new file mode 100644 index 00000000000..5b17ce45807 --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/SimpleFullyNullableRowForeignKeyValueFactory.cs @@ -0,0 +1,68 @@ +// 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 SimpleFullyNullableRowForeignKeyValueFactory : RowForeignKeyValueFactory +{ + private readonly IColumn _column; + private readonly ColumnAccessors _columnAccessors; + + /// + /// This is an internal API that supports the 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 SimpleFullyNullableRowForeignKeyValueFactory( + IForeignKeyConstraint foreignKey, + IColumn column, + ColumnAccessors columnAccessors) + : base(foreignKey) + { + _column = column; + _columnAccessors = columnAccessors; + EqualityComparer = CreateKeyEqualityComparer(column); + } + + /// + public override IEqualityComparer EqualityComparer { get; } + + /// + public override bool TryCreateDependentKeyValue(object?[] keyValues, [NotNullWhen(true)] out TKey? key) + { + key = (TKey?)keyValues[0]; + return key != null; + } + + /// + public override bool TryCreateDependentKeyValue( + IDictionary keyPropertyValues, [NotNullWhen(true)] out TKey? key) + { + if (keyPropertyValues.TryGetValue(_column.Name, out var value)) + { + key = (TKey?)value; + return key != null; + } + + key = default; + return false; + } + + /// + public override bool TryCreateDependentKeyValue(IReadOnlyModificationCommand command, bool fromOriginalValues, [NotNullWhen(true)] out TKey? key) + { + (key, var present) = fromOriginalValues + ? ((Func)_columnAccessors.OriginalValueGetter)(command) + : ((Func)_columnAccessors.CurrentValueGetter)(command); + return present && key != null; + } +} diff --git a/src/EFCore.Relational/Update/Internal/SimpleNonNullableRowForeignKeyValueFactory.cs b/src/EFCore.Relational/Update/Internal/SimpleNonNullableRowForeignKeyValueFactory.cs new file mode 100644 index 00000000000..b3a8275fbcc --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/SimpleNonNullableRowForeignKeyValueFactory.cs @@ -0,0 +1,68 @@ +// 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 SimpleNonNullableRowForeignKeyValueFactory : RowForeignKeyValueFactory +{ + private readonly IColumn _column; + private readonly ColumnAccessors _columnAccessors; + + /// + /// This is an internal API that supports the 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 SimpleNonNullableRowForeignKeyValueFactory( + IForeignKeyConstraint foreignKey, + IColumn column, + ColumnAccessors columnAccessors) + : base(foreignKey) + { + _column = column; + _columnAccessors = columnAccessors; + EqualityComparer = CreateKeyEqualityComparer(column); + } + + /// + public override IEqualityComparer EqualityComparer { get; } + + /// + public override bool TryCreateDependentKeyValue(object?[] keyValues, [NotNullWhen(true)] out TKey? key) + { + key = (TKey?)keyValues[0]!; + return true; + } + + /// + public override bool TryCreateDependentKeyValue( + IDictionary keyPropertyValues, [NotNullWhen(true)] out TKey? key) + { + if (keyPropertyValues.TryGetValue(_column.Name, out var value)) + { + key = (TKey?)value!; + return true; + } + + key = default; + return false; + } + + /// + public override bool TryCreateDependentKeyValue(IReadOnlyModificationCommand command, bool fromOriginalValues, [NotNullWhen(true)] out TKey? key) + { + (key, var present) = fromOriginalValues + ? ((Func)_columnAccessors.OriginalValueGetter)(command) + : ((Func)_columnAccessors.CurrentValueGetter)(command); + return present; + } +} diff --git a/src/EFCore.Relational/Update/Internal/SimpleNullablePrincipalRowForeignKeyValueFactory.cs b/src/EFCore.Relational/Update/Internal/SimpleNullablePrincipalRowForeignKeyValueFactory.cs new file mode 100644 index 00000000000..7a20de00c56 --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/SimpleNullablePrincipalRowForeignKeyValueFactory.cs @@ -0,0 +1,69 @@ +// 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 SimpleNullablePrincipalRowForeignKeyValueFactory : RowForeignKeyValueFactory + where TNonNullableKey : struct +{ + private readonly IColumn _column; + private readonly ColumnAccessors _columnAccessors; + + /// + /// This is an internal API that supports the 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 SimpleNullablePrincipalRowForeignKeyValueFactory( + IForeignKeyConstraint foreignKey, + IColumn column, + ColumnAccessors columnAccessors) + : base(foreignKey) + { + _column = column; + _columnAccessors = columnAccessors; + EqualityComparer = CreateKeyEqualityComparer(column); + } + + /// + public override IEqualityComparer EqualityComparer { get; } + + /// + public override bool TryCreateDependentKeyValue(object?[] keyValues, [NotNullWhen(true)] out TKey? key) + { + key = (TKey?)keyValues[0]!; + return true; + } + + /// + public override bool TryCreateDependentKeyValue( + IDictionary keyPropertyValues, [NotNullWhen(true)] out TKey? key) + { + if (keyPropertyValues.TryGetValue(_column.Name, out var value)) + { + key = (TKey?)value!; + return true; + } + + key = default; + return false; + } + + /// + public override bool TryCreateDependentKeyValue(IReadOnlyModificationCommand command, bool fromOriginalValues, [NotNullWhen(true)] out TKey? key) + { + (key, var present) = fromOriginalValues + ? ((Func)_columnAccessors.OriginalValueGetter)(command) + : ((Func)_columnAccessors.CurrentValueGetter)(command); + return present; + } +} diff --git a/src/EFCore.Relational/Update/Internal/SimpleNullableRowForeignKeyValueFactory.cs b/src/EFCore.Relational/Update/Internal/SimpleNullableRowForeignKeyValueFactory.cs new file mode 100644 index 00000000000..7ca69bc86a7 --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/SimpleNullableRowForeignKeyValueFactory.cs @@ -0,0 +1,79 @@ +// 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 SimpleNullableRowForeignKeyValueFactory : RowForeignKeyValueFactory + where TKey : struct +{ + private readonly IColumn _column; + private readonly ColumnAccessors _columnAccessors; + + /// + /// This is an internal API that supports the 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 SimpleNullableRowForeignKeyValueFactory( + IForeignKeyConstraint foreignKey, + IColumn column, + ColumnAccessors columnAccessors) + : base(foreignKey) + { + _column = column; + _columnAccessors = columnAccessors; + EqualityComparer = CreateKeyEqualityComparer(column); + } + + /// + public override IEqualityComparer EqualityComparer { get; } + + /// + public override bool TryCreateDependentKeyValue(object?[] keyValues, [NotNullWhen(true)] out TKey key) + { + return HandleNullableValue((TKey?)keyValues[0], out key); + } + + /// + public override bool TryCreateDependentKeyValue( + IDictionary keyPropertyValues, [NotNullWhen(true)] out TKey key) + { + if (keyPropertyValues.TryGetValue(_column.Name, out var value)) + { + return HandleNullableValue((TKey?)value, out key); + } + + key = default(TKey); + return false; + } + + /// + public override bool TryCreateDependentKeyValue(IReadOnlyModificationCommand command, bool fromOriginalValues, [NotNullWhen(true)] out TKey key) + { + var (keyValue, present) = fromOriginalValues + ? ((Func)_columnAccessors.OriginalValueGetter)(command) + : ((Func)_columnAccessors.CurrentValueGetter)(command); + return HandleNullableValue(present ? keyValue : null, out key); + } + + private static bool HandleNullableValue(TKey? value, out TKey key) + { + if (value.HasValue) + { + key = (TKey)value; + return true; + } + + key = default; + return false; + } +} diff --git a/src/EFCore.Relational/Update/Internal/SimpleRowIndexValueFactory.cs b/src/EFCore.Relational/Update/Internal/SimpleRowIndexValueFactory.cs new file mode 100644 index 00000000000..8e9d8521218 --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/SimpleRowIndexValueFactory.cs @@ -0,0 +1,111 @@ +// 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; +using Microsoft.EntityFrameworkCore.Internal; +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 SimpleRowIndexValueFactory : IRowIndexValueFactory +{ + private readonly IColumn _column; + private readonly ITableIndex _index; + private readonly ColumnAccessors _columnAccessors; + + /// + /// This is an internal API that supports the 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 SimpleRowIndexValueFactory(ITableIndex index) + { + _index = index; + _column = index.Columns.Single(); + _columnAccessors = ((Column)_column).Accessors; +#pragma warning disable EF1001 // Internal EF Core API usage. + EqualityComparer = NullableComparerAdapter.Wrap(_column.PropertyMappings.First().TypeMapping.ProviderValueComparer); +#pragma warning restore EF1001 // Internal EF Core API usage. + } + + /// + /// This is an internal API that supports the 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 TryCreateIndexValue(object?[] keyValues, [NotNullWhen(true)] out TKey? key) + { + key = (TKey?)keyValues[0]; + 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 TryCreateIndexValue(IDictionary keyValues, [NotNullWhen(true)] out TKey? key) + { + if (keyValues.TryGetValue(_column.Name, out var value)) + { + key = (TKey?)value; + return key != null; + } + + key = default; + return false; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool TryCreateIndexValue(IReadOnlyModificationCommand command, bool fromOriginalValues, [NotNullWhen(true)] out TKey? key) + { + (key, var present) = fromOriginalValues + ? ((Func)_columnAccessors.OriginalValueGetter)(command) + : ((Func)_columnAccessors.CurrentValueGetter)(command); + return present && 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 object? CreateValueIndex(IReadOnlyModificationCommand command, bool fromOriginalValues = false) + => TryCreateIndexValue(command, fromOriginalValues, out var keyValue) + ? new ValueIndex(_index, keyValue, EqualityComparer) + : 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[]? CreateValue(IReadOnlyModificationCommand command, bool fromOriginalValues = false) + => TryCreateIndexValue(command, fromOriginalValues, out var value) + ? (new object[] { value }) + : null; +} diff --git a/src/EFCore.Relational/Update/Internal/SimpleRowKeyValueFactory.cs b/src/EFCore.Relational/Update/Internal/SimpleRowKeyValueFactory.cs new file mode 100644 index 00000000000..1fbcf7d7153 --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/SimpleRowKeyValueFactory.cs @@ -0,0 +1,160 @@ +// 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; +using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; +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 SimpleRowKeyValueFactory : IRowKeyValueFactory +{ + private readonly IUniqueConstraint _constraint; + private readonly IColumn _column; + private readonly ColumnAccessors _columnAccessors; + + /// + /// This is an internal API that supports the 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(IUniqueConstraint constraint) + { + _constraint = constraint; + _column = constraint.Columns.Single(); + _columnAccessors = ((Column)_column).Accessors; + EqualityComparer = new NoNullsCustomEqualityComparer(_column.PropertyMappings.First().TypeMapping.ProviderValueComparer); + } + + /// + /// This is an internal API that supports the 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 TKey CreateKeyValue(object?[] keyValues) + { + var value = (TKey?)keyValues[0]; + if (value == null) + { + throw new InvalidOperationException( + RelationalStrings.NullKeyValue( + _constraint.Table.SchemaQualifiedName, + _column.Name)); + } + + return value; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual TKey CreateKeyValue(IDictionary keyValues) + { + var value = (TKey?)keyValues[_column.Name]; + if (value == null) + { + throw new InvalidOperationException( + RelationalStrings.NullKeyValue( + _constraint.Table.SchemaQualifiedName, + _column.Name)); + } + + return value; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual TKey CreateKeyValue(IReadOnlyModificationCommand command, bool fromOriginalValues = false) + { + var (key, found) = fromOriginalValues + ? ((Func)_columnAccessors.OriginalValueGetter)(command) + : ((Func)_columnAccessors.CurrentValueGetter)(command); + + if (!found) + { + throw new InvalidOperationException( + RelationalStrings.NullKeyValue( + _constraint.Table.SchemaQualifiedName, + _column.Name)); + } + + return 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 object CreateValueIndex(IReadOnlyModificationCommand command, bool fromOriginalValues = false) + => new ValueIndex( + _constraint, + CreateKeyValue(command, fromOriginalValues), + EqualityComparer); + + object[] IRowKeyValueFactory.CreateKeyValue(IReadOnlyModificationCommand command, bool fromOriginalValues) + => new object[] { CreateKeyValue(command, fromOriginalValues)! }; + + 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()) + { +#pragma warning disable EF1001 // Internal EF Core API usage. + comparer = comparer.ToNonNullNullableComparer(); +#pragma warning restore EF1001 // Internal EF Core API usage. + } + + _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); + } +} diff --git a/src/EFCore.Relational/Update/Internal/ValueIndex.cs b/src/EFCore.Relational/Update/Internal/ValueIndex.cs new file mode 100644 index 00000000000..47bea8a8d2c --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/ValueIndex.cs @@ -0,0 +1,61 @@ +// 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 sealed class ValueIndex +{ + private readonly object _metadata; + private readonly TKey _keyValue; + private readonly IEqualityComparer _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 ValueIndex( + object metadata, + TKey keyValue, + IEqualityComparer keyComparer) + { + _metadata = metadata; + _keyValue = keyValue; + _keyComparer = keyComparer; + } + + private bool Equals(ValueIndex other) + => other._metadata == _metadata + && _keyComparer.Equals(_keyValue, other._keyValue); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override bool Equals(object? obj) + => ReferenceEquals(this, obj) + || (obj is ValueIndex other && Equals(other)); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(_metadata); + hash.Add(_keyValue, _keyComparer); + return hash.ToHashCode(); + } +} diff --git a/src/EFCore.Relational/Update/ModificationCommand.cs b/src/EFCore.Relational/Update/ModificationCommand.cs index 52adf65a353..280eec7b7b4 100644 --- a/src/EFCore.Relational/Update/ModificationCommand.cs +++ b/src/EFCore.Relational/Update/ModificationCommand.cs @@ -34,38 +34,46 @@ public class ModificationCommand : IModificationCommand /// Creation parameters. public ModificationCommand(in ModificationCommandParameters modificationCommandParameters) { + Table = modificationCommandParameters.Table; TableName = modificationCommandParameters.TableName; Schema = modificationCommandParameters.Schema; _generateParameterName = modificationCommandParameters.GenerateParameterName; _sensitiveLoggingEnabled = modificationCommandParameters.SensitiveLoggingEnabled; _comparer = modificationCommandParameters.Comparer; _logger = modificationCommandParameters.Logger; + EntityState = EntityState.Modified; } - /// - /// The name of the table containing the data to be modified. - /// + /// + public virtual ITable? Table { get; } + + /// public virtual string TableName { get; } - /// - /// The schema containing the table, or to use the default schema. - /// + /// public virtual string? Schema { get; } - /// - /// The s that represent the entities that are mapped to the row - /// to update. - /// + /// public virtual IReadOnlyList Entries => _entries; + /// + public virtual EntityState EntityState { get; private set; } + /// - /// The that indicates whether the row will be - /// inserted (), - /// updated (), - /// or deleted ((). + /// Indicates whether the database will return values for some mapped properties + /// that will then need to be propagated back to the tracked entities. /// - public virtual EntityState EntityState { get; private set; } = EntityState.Modified; + public virtual bool RequiresResultPropagation + { + get + { + // ReSharper disable once AssignmentIsFullyDiscarded + _ = ColumnModifications; + + return _requiresResultPropagation; + } + } /// /// The list of needed to perform the insert, update, or delete. @@ -90,27 +98,7 @@ public virtual void AssertColumnsNotInitialized() } } - /// - /// Indicates whether the database will return values for some mapped properties - /// that will then need to be propagated back to the tracked entities. - /// - public virtual bool RequiresResultPropagation - { - get - { - // ReSharper disable once AssignmentIsFullyDiscarded - _ = ColumnModifications; - - return _requiresResultPropagation; - } - } - - /// - /// This is an internal API that supports the 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 AddEntry(IUpdateEntry entry, bool mainEntry) { AssertColumnsNotInitialized(); @@ -271,9 +259,7 @@ private List GenerateColumnModifications() foreach (var entry in _entries) { - var nonMainEntry = updating - && (entry.EntityState == EntityState.Deleted - || entry.EntityState == EntityState.Added); + var nonMainEntry = !_mainEntryAdded || entry != _entries[0]; var tableMapping = GetTableMapping(entry.EntityType); if (tableMapping == null) @@ -290,7 +276,7 @@ private List GenerateColumnModifications() foreach (var columnMapping in tableMapping.ColumnMappings) { var property = columnMapping.Property; - var column = (IColumn)columnMapping.Column; + var column = columnMapping.Column; var isKey = property.IsPrimaryKey(); var isCondition = !adding && (isKey || property.IsConcurrencyToken); var readValue = state != EntityState.Deleted && entry.IsStoreGenerated(property); @@ -308,7 +294,7 @@ private List GenerateColumnModifications() else if ((updating && property.GetAfterSaveBehavior() == PropertySaveBehavior.Save) || (!isKey && nonMainEntry)) { - writeValue = columnPropagator?.TryPropagate(property, entry) + writeValue = columnPropagator?.TryPropagate(columnMapping, entry) ?? (entry.EntityState == EntityState.Added || entry.IsModified(property)); } } @@ -383,12 +369,12 @@ private List GenerateColumnModifications() return columnModifications; } - private ITableMappingBase? GetTableMapping(IEntityType entityType) + private ITableMapping? GetTableMapping(IEntityType entityType) { - ITableMappingBase? tableMapping = null; + ITableMapping? tableMapping = null; foreach (var mapping in entityType.GetTableMappings()) { - var table = ((ITableMappingBase)mapping).Table; + var table = mapping.Table; if (table.Name == TableName && table.Schema == Schema) { @@ -402,7 +388,7 @@ private List GenerateColumnModifications() private static void InitializeSharedColumns( IUpdateEntry entry, - ITableMappingBase tableMapping, + ITableMapping tableMapping, bool updating, Dictionary columnMap) { @@ -417,7 +403,7 @@ private static void InitializeSharedColumns( if (updating) { - columnPropagator.RecordValue(columnMapping.Property, entry); + columnPropagator.RecordValue(columnMapping, entry); } } } @@ -460,8 +446,9 @@ private sealed class ColumnValuePropagator public IColumnModification? ColumnModification { get; set; } - public void RecordValue(IProperty property, IUpdateEntry entry) + public void RecordValue(IColumnMapping mapping, IUpdateEntry entry) { + var property = mapping.Property; switch (entry.EntityState) { case EntityState.Modified: @@ -469,17 +456,17 @@ public void RecordValue(IProperty property, IUpdateEntry entry) && entry.IsModified(property)) { _write = true; - _currentValue = entry.GetCurrentValue(property); + _currentValue = entry.GetCurrentProviderValue(property); } break; case EntityState.Added: - _currentValue = entry.GetCurrentValue(property); - _write = !property.GetValueComparer().Equals(_originalValue, _currentValue); + _currentValue = entry.GetCurrentProviderValue(property); + _write = !mapping.TypeMapping.ProviderValueComparer.Equals(_originalValue, _currentValue); break; case EntityState.Deleted: - _originalValue = entry.GetOriginalValue(property); + _originalValue = entry.GetOriginalProviderValue(property); if (!_write && !property.IsPrimaryKey()) { @@ -491,15 +478,20 @@ public void RecordValue(IProperty property, IUpdateEntry entry) } } - public bool TryPropagate(IProperty property, IUpdateEntry entry) + public bool TryPropagate(IColumnMapping mapping, IUpdateEntry entry) { + var property = mapping.Property; if (_write && (entry.EntityState == EntityState.Unchanged || (entry.EntityState == EntityState.Modified && !entry.IsModified(property)) || (entry.EntityState == EntityState.Added - && property.GetValueComparer().Equals(_originalValue, entry.GetCurrentValue(property))))) + && mapping.TypeMapping.ProviderValueComparer.Equals(_originalValue, entry.GetCurrentValue(property))))) { - entry.SetStoreGeneratedValue(property, _currentValue); + if (property.GetAfterSaveBehavior() == PropertySaveBehavior.Save + || entry.EntityState == EntityState.Added) + { + entry.SetStoreGeneratedValue(property, _currentValue); + } return false; } diff --git a/src/EFCore.Relational/Update/ModificationCommandParameters.cs b/src/EFCore.Relational/Update/ModificationCommandParameters.cs index edf3b3c7b84..d8bb902882d 100644 --- a/src/EFCore.Relational/Update/ModificationCommandParameters.cs +++ b/src/EFCore.Relational/Update/ModificationCommandParameters.cs @@ -34,6 +34,7 @@ public ModificationCommandParameters( Func? generateParameterName = null, IDiagnosticsLogger? logger = null) { + Table = null; TableName = tableName; Schema = schemaName; GenerateParameterName = generateParameterName; @@ -42,6 +43,30 @@ public ModificationCommandParameters( Logger = logger; } + /// + /// Creates a new instance. + /// + /// The table containing the data to be modified. + /// Indicates whether potentially sensitive data (e.g. database values) can be logged. + /// An for . + /// A delegate to generate parameter names. + /// An for . + public ModificationCommandParameters( + ITable table, + bool sensitiveLoggingEnabled, + IComparer? comparer = null, + Func? generateParameterName = null, + IDiagnosticsLogger? logger = null) + { + Table = table; + TableName = table.Name; + Schema = table.Schema; + GenerateParameterName = generateParameterName; + SensitiveLoggingEnabled = sensitiveLoggingEnabled; + Comparer = comparer; + Logger = logger; + } + /// /// The name of the table containing the data to be modified. /// @@ -52,6 +77,11 @@ public ModificationCommandParameters( /// public string? Schema { get; init; } + /// + /// The table containing the data to be modified. + /// + public ITable? Table { get; init; } + /// /// A delegate to generate parameter names. /// 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/InternalEntityEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs index 94d13cf4851..444ecec374d 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs @@ -667,22 +667,21 @@ private CurrentValueType GetValueType( return CurrentValueType.Normal; } - equals ??= ValuesEqualFunc(property); var defaultValue = property.ClrType.GetDefaultValue(); var value = ReadPropertyValue(property); - if (!equals(value, defaultValue)) + if (!AreEqual(value, defaultValue, property, equals)) { return CurrentValueType.Normal; } if (_storeGeneratedValues.TryGetValue(tempIndex, out value) - && !equals(value, defaultValue)) + && !AreEqual(value, defaultValue, property, equals)) { return CurrentValueType.StoreGenerated; } if (_temporaryValues.TryGetValue(tempIndex, out value) - && !equals(value, defaultValue)) + && !AreEqual(value, defaultValue, property, equals)) { return CurrentValueType.Temporary; } @@ -1167,19 +1166,17 @@ public object? this[IPropertyBase propertyBase] var defaultValue = propertyClrType.GetDefaultValue(); var property = (IProperty)propertyBase; - var equals = ValuesEqualFunc(property); - if (_storeGeneratedValues.TryGetValue(storeGeneratedIndex, out var generatedValue) - && !equals(generatedValue, defaultValue)) + && !AreEqual(generatedValue, defaultValue, property)) { return generatedValue; } var value = ReadPropertyValue(propertyBase); - if (equals(value, defaultValue)) + if (AreEqual(value, defaultValue, property)) { if (_temporaryValues.TryGetValue(storeGeneratedIndex, out generatedValue) - && !equals(generatedValue, defaultValue)) + && !AreEqual(generatedValue, defaultValue, property)) { return generatedValue; } @@ -1221,22 +1218,21 @@ private void SetProperty( var asProperty = propertyBase as IProperty; int propertyIndex; CurrentValueType currentValueType; - Func equals; - + + var valuesEqual = false; if (asProperty != null) { propertyIndex = asProperty.GetIndex(); - equals = ValuesEqualFunc(asProperty); - currentValueType = GetValueType(asProperty, equals); + valuesEqual = AreEqual(currentValue, value, asProperty); + currentValueType = GetValueType(asProperty); } else { propertyIndex = -1; - equals = ReferenceEquals; + valuesEqual = ReferenceEquals(currentValue, value); currentValueType = CurrentValueType.Normal; } - var valuesEqual = equals(currentValue, value); if (!valuesEqual || (propertyIndex != -1 @@ -1304,7 +1300,7 @@ private void SetProperty( if (valueType == CurrentValueType.StoreGenerated) { var defaultValue = asProperty!.ClrType.GetDefaultValue(); - if (!equals(currentValue, defaultValue)) + if (!AreEqual(currentValue, defaultValue, asProperty!)) { WritePropertyValue(asProperty, defaultValue, isMaterialization); } @@ -1315,13 +1311,13 @@ private void SetProperty( else { var defaultValue = asProperty!.ClrType.GetDefaultValue(); - if (!equals(currentValue, defaultValue)) + if (!AreEqual(currentValue, defaultValue, asProperty!)) { WritePropertyValue(asProperty, defaultValue, isMaterialization); } if (_storeGeneratedValues.TryGetValue(storeGeneratedIndex, out var generatedValue) - && !equals(generatedValue, defaultValue)) + && !AreEqual(generatedValue, defaultValue, asProperty!)) { _storeGeneratedValues.SetValue(asProperty, defaultValue, storeGeneratedIndex); } @@ -1391,8 +1387,13 @@ public void HandleNullForeignKey( } } - private static Func ValuesEqualFunc(IProperty property) - => property.GetValueComparer().Equals; + private static bool AreEqual(object? value, object? otherValue, IProperty property) + => property.GetValueComparer().Equals(value, otherValue); + + private static bool AreEqual(object? value, object? otherValue, IProperty property, Func? equals) + => equals != null + ? equals(value, otherValue) + : AreEqual(value, otherValue, property); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -1410,9 +1411,8 @@ public void AcceptChanges() if (storeGeneratedIndex != -1 && _storeGeneratedValues.TryGetValue(storeGeneratedIndex, out var value)) { - var equals = ValuesEqualFunc(property); var defaultValue = property.ClrType.GetDefaultValue(); - if (!equals(value, defaultValue)) + if (!AreEqual(value, defaultValue, property)) { this[property] = value; } @@ -1685,12 +1685,11 @@ public bool HasDefaultValue(IProperty property) } var defaultValue = property.ClrType.GetDefaultValue(); - var equals = ValuesEqualFunc(property); return (!_storeGeneratedValues.TryGetValue(storeGeneratedIndex, out var generatedValue) - || equals(defaultValue, generatedValue)) + || AreEqual(defaultValue, generatedValue, property)) && (!_temporaryValues.TryGetValue(storeGeneratedIndex, out generatedValue) - || equals(defaultValue, generatedValue)); + || AreEqual(defaultValue, generatedValue, property)); } /// 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/src/EFCore/Internal/NullableComparerAdapter.cs b/src/EFCore/Internal/NullableComparerAdapter.cs new file mode 100644 index 00000000000..aaef157045d --- /dev/null +++ b/src/EFCore/Internal/NullableComparerAdapter.cs @@ -0,0 +1,48 @@ +// 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; + +namespace Microsoft.EntityFrameworkCore.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public sealed class NullableComparerAdapter : IEqualityComparer +{ + private readonly IEqualityComparer _comparer; + + /// + /// This is an internal API that supports the 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 NullableComparerAdapter(IEqualityComparer comparer) + { + _comparer = comparer; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static IEqualityComparer Wrap(IEqualityComparer comparer) + => comparer is IEqualityComparer nullableComparer + ? nullableComparer + : new NullableComparerAdapter(comparer); + + /// + public bool Equals(TNullableKey? x, TNullableKey? y) + => (x is null && y is null) + || (x is not null && y is not null && _comparer.Equals(x, y)); + + /// + public int GetHashCode(TNullableKey obj) + => obj is null ? 0 : _comparer.GetHashCode(obj); +} diff --git a/src/EFCore/Metadata/IProperty.cs b/src/EFCore/Metadata/IProperty.cs index 206b629563a..47aa28b8049 100644 --- a/src/EFCore/Metadata/IProperty.cs +++ b/src/EFCore/Metadata/IProperty.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections; +using Microsoft.EntityFrameworkCore.Internal; namespace Microsoft.EntityFrameworkCore.Metadata; @@ -24,30 +25,7 @@ public interface IProperty : IReadOnlyProperty, IPropertyBase /// The property type. /// A new equality comparer. IEqualityComparer CreateKeyEqualityComparer() - { - var comparer = GetKeyValueComparer(); - - return comparer is IEqualityComparer nullableComparer - ? nullableComparer - : new NullableComparer(comparer); - } - - private sealed class NullableComparer : IEqualityComparer - { - private readonly IEqualityComparer _comparer; - - public NullableComparer(IEqualityComparer comparer) - { - _comparer = comparer; - } - - public bool Equals(TNullableKey? x, TNullableKey? y) - => (x == null && y == null) - || (x != null && y != null && _comparer.Equals(x, y)); - - public int GetHashCode(TNullableKey obj) - => obj is null ? 0 : _comparer.GetHashCode(obj); - } + => NullableComparerAdapter.Wrap(GetKeyValueComparer()); /// /// Finds the first principal property that the given property is constrained by diff --git a/src/EFCore/Update/UpdateEntryExtensions.cs b/src/EFCore/Update/UpdateEntryExtensions.cs index d56dc5021f6..233c1ea99bc 100644 --- a/src/EFCore/Update/UpdateEntryExtensions.cs +++ b/src/EFCore/Update/UpdateEntryExtensions.cs @@ -6,7 +6,6 @@ using System.Text; using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Metadata.Internal; -using System; namespace Microsoft.EntityFrameworkCore.Update; @@ -42,6 +41,29 @@ public static class UpdateEntryExtensions return value; } + /// + /// Gets the original value that was assigned to the property and converts it to the provider-expected value. + /// + /// The entry. + /// The property to get the value for. + /// The value for the property. + public static object? GetOriginalProviderValue(this IUpdateEntry updateEntry, IProperty property) + { + var value = updateEntry.GetOriginalValue(property); + var typeMapping = property.GetTypeMapping(); + value = value?.GetType().IsInteger() == true && typeMapping.ClrType.UnwrapNullableType().IsEnum + ? Enum.ToObject(typeMapping.ClrType.UnwrapNullableType(), value) + : value; + + var converter = typeMapping.Converter; + if (converter != null) + { + value = converter.ConvertToProvider(value); + } + + return value; + } + /// /// /// Creates a human-readable representation of the given . diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index 3e2472cb2f4..9c8698938ae 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/Metadata/RelationalModelTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs index ae8222c0982..521377ffd27 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs @@ -489,6 +489,7 @@ private static void AssertTables(IRelationalModel model, Mapping mapping) Assert.Equal("default_datetime_mapping", orderDateColumn.StoreType); Assert.False(orderDateColumn.IsNullable); Assert.Same(ordersTable, orderDateColumn.Table); + Assert.Same(orderDateMapping, orderDateColumn.FindColumnMapping(orderType)); var orderPk = orderType.FindPrimaryKey(); var orderPkConstraint = orderPk.GetMappedConstraints().Single(); @@ -536,6 +537,7 @@ private static void AssertTables(IRelationalModel model, Mapping mapping) Assert.Equal("FK_DateDetails", orderDateFkConstraint.Name); Assert.Equal(nameof(Order.OrderDate), orderDateFkConstraint.Columns.Single().Name); Assert.Equal(nameof(DateDetails.Date), orderDateFkConstraint.PrincipalColumns.Single().Name); + Assert.Equal("PK_DateDetails", orderDateFkConstraint.PrincipalUniqueConstraint.Name); Assert.Equal("DateDetails", orderDateFkConstraint.PrincipalTable.Name); var orderCustomerFk = orderType.GetForeignKeys().Single(fk => fk.PrincipalEntityType.ClrType == typeof(Customer)); @@ -718,6 +720,7 @@ private static void AssertTables(IRelationalModel model, Mapping mapping) Assert.Equal(ReferentialAction.Cascade, orderCustomerFkConstraint.OnDeleteAction); Assert.Equal(orderCustomerFk, orderCustomerFkConstraint.MappedForeignKeys.Single()); Assert.Equal(new[] { orderDateFkConstraint, orderCustomerFkConstraint }, ordersTable.ForeignKeyConstraints); + Assert.Empty(ordersTable.ReferencingForeignKeyConstraints); Assert.Equal(orderCustomerFkConstraint.Name, orderCustomerFk.GetConstraintName()); Assert.Equal(orderCustomerFkConstraint.Name, orderCustomerFk.GetConstraintName( @@ -751,6 +754,8 @@ private static void AssertTables(IRelationalModel model, Mapping mapping) Assert.NotNull(anotherSpecialCustomerFkConstraint.MappedForeignKeys.Single()); Assert.Same(specialCustomerTable, anotherSpecialCustomerFkConstraint.PrincipalTable); + Assert.Equal(new[] { orderCustomerFkConstraint, specialCustomerTptFkConstraint }, customerTable.ReferencingForeignKeyConstraints); + var specialCustomerDbIndex = specialCustomerTable.Indexes.Last(); Assert.Equal("IX_SpecialCustomer_RelatedCustomerSpeciality", specialCustomerDbIndex.Name); Assert.NotNull(specialCustomerDbIndex.MappedIndexes.Single()); @@ -811,6 +816,7 @@ private static void AssertTables(IRelationalModel model, Mapping mapping) Assert.Equal(ReferentialAction.Cascade, orderCustomerFkConstraint.OnDeleteAction); Assert.Equal(orderCustomerFk, orderCustomerFkConstraint.MappedForeignKeys.Single()); Assert.Equal(new[] { orderDateFkConstraint, orderCustomerFkConstraint }, ordersTable.ForeignKeyConstraints); + Assert.Empty(ordersTable.ReferencingForeignKeyConstraints); Assert.Equal(orderCustomerFkConstraint.Name, orderCustomerFk.GetConstraintName()); Assert.Equal(orderCustomerFkConstraint.Name, orderCustomerFk.GetConstraintName( @@ -832,6 +838,9 @@ private static void AssertTables(IRelationalModel model, Mapping mapping) Assert.Equal("FK_AbstractBase_AbstractBase_AnotherRelatedCustomerId", anotherSpecialCustomerFkConstraint.Name); Assert.NotNull(anotherSpecialCustomerFkConstraint.MappedForeignKeys.Single()); + Assert.Equal(new[] { anotherSpecialCustomerFkConstraint, specialCustomerFkConstraint, orderCustomerFkConstraint }, + customerTable.ReferencingForeignKeyConstraints); + Assert.Equal("IX_AbstractBase_RelatedCustomerSpeciality", specialCustomerDbIndex.Name); Assert.Equal("IX_AbstractBase_AnotherRelatedCustomerId", anotherSpecialCustomerDbIndex.Name); @@ -847,6 +856,9 @@ private static void AssertTables(IRelationalModel model, Mapping mapping) Assert.False(specialCustomerTypeMapping.IncludesDerivedTypes); Assert.NotSame(customerTable, specialCustomerTable); + Assert.Empty(ordersTable.ReferencingForeignKeyConstraints); + Assert.Empty(customerTable.ReferencingForeignKeyConstraints); + Assert.True(customerTable.EntityTypeMappings.Single().IsSharedTablePrincipal); Assert.Equal(5, customerTable.Columns.Count()); diff --git a/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs b/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs index cf221e4cac1..4cbd36ae702 100644 --- a/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs +++ b/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs @@ -975,7 +975,6 @@ public ICommandBatchPreparer CreateCommandBatchPreparer( modificationCommandBatchFactory, new ParameterNameGeneratorFactory(new ParameterNameGeneratorDependencies()), new ModificationCommandComparer(), - new KeyValueIndexFactorySource(), new ModificationCommandFactory(), loggingOptions, new FakeDiagnosticsLogger(), diff --git a/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs b/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs index 870a0086f1e..cd639c580e5 100644 --- a/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs +++ b/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs @@ -254,7 +254,7 @@ public Fuel(double volume) public double Volume { get; } } - [ConditionalFact] + [ConditionalFact(Skip = "Issue #27738")] public virtual void Can_insert_and_read_back_with_case_insensitive_string_key() { using (var context = CreateContext()) diff --git a/test/EFCore.SqlServer.FunctionalTests/TPTTableSplittingSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/TPTTableSplittingSqlServerTest.cs index 518ec2d7f57..482777e01e9 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TPTTableSplittingSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TPTTableSplittingSqlServerTest.cs @@ -184,22 +184,17 @@ public override async Task Can_change_dependent_instance_non_derived() { await base.Can_change_dependent_instance_non_derived(); AssertSql( - @"@p1='Trek Pro Fit Madone 6 Series' (Nullable = false) (Size = 450) -@p0='repairman' (Size = 4000) + @"@p0='Trek Pro Fit Madone 6 Series' (Nullable = false) (Size = 450) +@p1='Repair' (Size = 4000) +@p3='Trek Pro Fit Madone 6 Series' (Nullable = false) (Size = 450) +@p2='repairman' (Size = 4000) -SET IMPLICIT_TRANSACTIONS OFF; -SET NOCOUNT ON; -UPDATE [Vehicles] SET [Operator_Name] = @p0 -OUTPUT 1 -WHERE [Name] = @p1;", - // - @"@p2='Trek Pro Fit Madone 6 Series' (Nullable = false) (Size = 450) -@p3='Repair' (Size = 4000) - -SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; INSERT INTO [LicensedOperators] ([VehicleName], [LicenseType]) -VALUES (@p2, @p3);", +VALUES (@p0, @p1); +UPDATE [Vehicles] SET [Operator_Name] = @p2 +OUTPUT 1 +WHERE [Name] = @p3;", // @"SELECT TOP(2) [v].[Name], [v].[SeatingCapacity], [c].[AttachedVehicleName], CASE WHEN [c].[Name] IS NOT NULL THEN N'CompositeVehicle' 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() {