From f7f0d9ac2e358364ea065a357d77beb6d90cef9b Mon Sep 17 00:00:00 2001 From: AndriySvyryd Date: Tue, 4 Feb 2020 13:40:43 -0800 Subject: [PATCH] Add a relational model API Use the new model in migrations and update pipeline Part of #12846, #2725, #8258, #15671, #17270 --- .../Design/CSharpSnapshotGenerator.cs | 21 +- .../Design/MigrationsCodeGenerator.cs | 5 +- .../Internal/SnapshotModelProcessor.cs | 17 +- .../Internal/CSharpDbContextGenerator.cs | 9 +- .../RelationalEntityTypeExtensions.cs | 40 +++ .../Extensions/RelationalModelExtensions.cs | 20 ++ .../RelationalPropertyExtensions.cs | 8 + .../RelationalModelValidator.cs | 8 +- .../RelationalConventionSetBuilder.cs | 5 + .../Conventions/RelationalModelConvention.cs | 17 + .../Conventions/SharedTableConvention.cs | 4 + src/EFCore.Relational/Metadata/IColumn.cs | 23 ++ src/EFCore.Relational/Metadata/IColumnBase.cs | 40 +++ .../Metadata/IColumnMapping.cs | 21 ++ .../Metadata/IColumnMappingBase.cs | 34 ++ src/EFCore.Relational/Metadata/ITable.cs | 65 ++++ src/EFCore.Relational/Metadata/ITableBase.cs | 34 ++ .../Metadata/ITableMapping.cs | 23 ++ .../Metadata/ITableMappingBase.cs | 34 ++ .../Metadata/Internal/Column.cs | 81 +++++ .../Metadata/Internal/ColumnExtensions.cs | 63 ++++ .../Metadata/Internal/ColumnMapping.cs | 71 ++++ .../Internal/ColumnMappingComparer.cs | 76 +++++ .../Internal/ColumnMappingExtensions.cs | 52 +++ .../Metadata/Internal/RelationalModel.cs | 159 +++++++++ .../Internal/RelationalPropertyExtensions.cs | 5 +- .../Metadata/Internal/Table.cs | 130 ++++++++ .../Metadata/Internal/TableExtensions.cs | 81 +++++ .../Metadata/Internal/TableMapping.cs | 186 ++--------- .../Metadata/Internal/TableMappingComparer.cs | 105 ++++++ .../Internal/TableMappingExtensions.cs | 57 ++++ .../Metadata/RelationalAnnotationNames.cs | 23 +- .../Internal/MigrationsModelDiffer.cs | 305 ++++++++++-------- .../Operations/DeleteDataOperation.cs | 9 +- .../Operations/InsertDataOperation.cs | 9 +- .../Operations/UpdateDataOperation.cs | 13 +- .../Query/SqlExpressions/SelectExpression.cs | 4 +- .../Update/Internal/CommandBatchPreparer.cs | 24 +- .../Update/Internal/SharedTableEntryMap.cs | 155 +++------ src/EFCore/Infrastructure/Annotatable.cs | 9 +- src/EFCore/Metadata/Internal/Model.cs | 8 +- .../Design/MigrationScaffolderTest.cs | 12 +- .../Design/SnapshotModelProcessorTest.cs | 21 +- .../Migrations/ModelSnapshotSqlServerTest.cs | 21 +- .../MigrationsInfrastructureTestBase.cs | 23 +- .../MigrationsTestBase.cs | 1 + .../Metadata/RelationalModelTest.cs | 145 +++++++++ .../Internal/MigrationsModelDifferTestBase.cs | 13 +- .../TestUtilities/TestTypeMappingSource.cs | 21 ++ .../Query/InheritanceSqlServerTest.cs | 8 +- 50 files changed, 1812 insertions(+), 506 deletions(-) create mode 100644 src/EFCore.Relational/Metadata/Conventions/RelationalModelConvention.cs create mode 100644 src/EFCore.Relational/Metadata/IColumn.cs create mode 100644 src/EFCore.Relational/Metadata/IColumnBase.cs create mode 100644 src/EFCore.Relational/Metadata/IColumnMapping.cs create mode 100644 src/EFCore.Relational/Metadata/IColumnMappingBase.cs create mode 100644 src/EFCore.Relational/Metadata/ITable.cs create mode 100644 src/EFCore.Relational/Metadata/ITableBase.cs create mode 100644 src/EFCore.Relational/Metadata/ITableMapping.cs create mode 100644 src/EFCore.Relational/Metadata/ITableMappingBase.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/Column.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/ColumnExtensions.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/ColumnMapping.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/ColumnMappingComparer.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/ColumnMappingExtensions.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/RelationalModel.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/Table.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/TableExtensions.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/TableMappingComparer.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/TableMappingExtensions.cs create mode 100644 test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs create mode 100644 test/EFCore.Specification.Tests/TestUtilities/TestTypeMappingSource.cs diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index 67612bcd6f0..6c780f76247 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -55,6 +55,15 @@ public virtual void Generate(string builderName, IModel model, IndentedStringBui var annotations = model.GetAnnotations().ToList(); + IgnoreAnnotationTypes(annotations, RelationalAnnotationNames.DbFunction); + IgnoreAnnotations( + annotations, + ChangeDetector.SkipDetectChangesAnnotation, + CoreAnnotationNames.ChangeTrackingStrategy, + CoreAnnotationNames.OwnedTypes, + RelationalAnnotationNames.CheckConstraints, + RelationalAnnotationNames.Tables); + if (annotations.Count > 0) { stringBuilder.Append(builderName); @@ -65,14 +74,6 @@ public virtual void Generate(string builderName, IModel model, IndentedStringBui ref annotations, RelationalAnnotationNames.DefaultSchema, nameof(RelationalModelBuilderExtensions.HasDefaultSchema), stringBuilder); - IgnoreAnnotationTypes(annotations, RelationalAnnotationNames.DbFunction); - IgnoreAnnotations( - annotations, - ChangeDetector.SkipDetectChangesAnnotation, - CoreAnnotationNames.ChangeTrackingStrategy, - CoreAnnotationNames.OwnedTypes, - RelationalAnnotationNames.CheckConstraints); - GenerateAnnotations(annotations, stringBuilder); } @@ -495,6 +496,7 @@ protected virtual void GeneratePropertyAnnotations([NotNull] IProperty property, IgnoreAnnotations( annotations, RelationalAnnotationNames.ColumnType, + RelationalAnnotationNames.TableColumnMappings, CoreAnnotationNames.ValueGeneratorFactory, CoreAnnotationNames.PropertyAccessMode, CoreAnnotationNames.ChangeTrackingStrategy, @@ -784,7 +786,8 @@ protected virtual void GenerateEntityTypeAnnotations( CoreAnnotationNames.ConstructorBinding, CoreAnnotationNames.DefiningQuery, CoreAnnotationNames.QueryFilter, - RelationalAnnotationNames.CheckConstraints); + RelationalAnnotationNames.CheckConstraints, + RelationalAnnotationNames.TableMappings); if (annotations.Count > 0) { diff --git a/src/EFCore.Design/Migrations/Design/MigrationsCodeGenerator.cs b/src/EFCore.Design/Migrations/Design/MigrationsCodeGenerator.cs index 1870afc9c35..1fad485aec6 100644 --- a/src/EFCore.Design/Migrations/Design/MigrationsCodeGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/MigrationsCodeGenerator.cs @@ -241,7 +241,10 @@ private IEnumerable GetAnnotationNamespaces(IEnumerable it CoreAnnotationNames.ValueGeneratorFactory, CoreAnnotationNames.DefiningQuery, CoreAnnotationNames.QueryFilter, - RelationalAnnotationNames.CheckConstraints + RelationalAnnotationNames.CheckConstraints, + RelationalAnnotationNames.Tables, + RelationalAnnotationNames.TableMappings, + RelationalAnnotationNames.TableColumnMappings }; var ignoredAnnotationTypes = new List diff --git a/src/EFCore.Design/Migrations/Internal/SnapshotModelProcessor.cs b/src/EFCore.Design/Migrations/Internal/SnapshotModelProcessor.cs index aa30e069f5f..2715341fe16 100644 --- a/src/EFCore.Design/Migrations/Internal/SnapshotModelProcessor.cs +++ b/src/EFCore.Design/Migrations/Internal/SnapshotModelProcessor.cs @@ -10,6 +10,8 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Conventions; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace Microsoft.EntityFrameworkCore.Migrations.Internal @@ -23,6 +25,7 @@ namespace Microsoft.EntityFrameworkCore.Migrations.Internal public class SnapshotModelProcessor : ISnapshotModelProcessor { private readonly IOperationReporter _operationReporter; + private readonly ProviderConventionSetBuilderDependencies _conventionDependencies; private readonly HashSet _relationalNames; /// @@ -31,9 +34,12 @@ public class SnapshotModelProcessor : ISnapshotModelProcessor /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public SnapshotModelProcessor([NotNull] IOperationReporter operationReporter) + public SnapshotModelProcessor( + [NotNull] IOperationReporter operationReporter, + [NotNull] ProviderConventionSetBuilderDependencies conventionDependencies) { _operationReporter = operationReporter; + _conventionDependencies = conventionDependencies; _relationalNames = new HashSet( typeof(RelationalAnnotationNames) .GetRuntimeFields() @@ -75,6 +81,15 @@ public virtual IModel Process(IModel model) } } + if (model is IConventionModel conventionModel + && _conventionDependencies != null) + { + var typeMappingConvention = new TypeMappingConvention(_conventionDependencies); + typeMappingConvention.ProcessModelFinalizing(conventionModel.Builder, null); + + model = new RelationalModelConvention().ProcessModelFinalized(conventionModel); + } + return model is IMutableModel mutableModel ? mutableModel.FinalizeModel() : model; diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs index 41091b95505..f1c293cf800 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs @@ -263,6 +263,7 @@ protected virtual void GenerateOnModelCreating( RemoveAnnotation(ref annotations, ChangeDetector.SkipDetectChangesAnnotation); RemoveAnnotation(ref annotations, RelationalAnnotationNames.MaxIdentifierLength); RemoveAnnotation(ref annotations, RelationalAnnotationNames.CheckConstraints); + RemoveAnnotation(ref annotations, RelationalAnnotationNames.Tables); RemoveAnnotation(ref annotations, ScaffoldingAnnotationNames.DatabaseName); RemoveAnnotation(ref annotations, ScaffoldingAnnotationNames.EntityTypeErrors); @@ -364,9 +365,10 @@ private void GenerateEntityType(IEntityType entityType, bool useDataAnnotations) RemoveAnnotation(ref annotations, RelationalAnnotationNames.TableName); RemoveAnnotation(ref annotations, RelationalAnnotationNames.Comment); RemoveAnnotation(ref annotations, RelationalAnnotationNames.Schema); + RemoveAnnotation(ref annotations, RelationalAnnotationNames.TableMappings); RemoveAnnotation(ref annotations, ScaffoldingAnnotationNames.DbSetName); - RemoveAnnotation(ref annotations, RelationalAnnotationNames.ViewDefinition); + RemoveAnnotation(ref annotations, RelationalAnnotationNames.ViewDefinition); var isView = entityType.FindAnnotation(RelationalAnnotationNames.ViewDefinition) != null; if (!useDataAnnotations || isView) { @@ -603,16 +605,17 @@ private void GenerateProperty(IProperty property, bool useDataAnnotations) var annotations = property.GetAnnotations().ToList(); - RemoveAnnotation(ref annotations, RelationalAnnotationNames.ColumnName); - RemoveAnnotation(ref annotations, RelationalAnnotationNames.ColumnType); RemoveAnnotation(ref annotations, CoreAnnotationNames.MaxLength); RemoveAnnotation(ref annotations, CoreAnnotationNames.TypeMapping); RemoveAnnotation(ref annotations, CoreAnnotationNames.Unicode); + RemoveAnnotation(ref annotations, RelationalAnnotationNames.ColumnName); + RemoveAnnotation(ref annotations, RelationalAnnotationNames.ColumnType); RemoveAnnotation(ref annotations, RelationalAnnotationNames.DefaultValue); RemoveAnnotation(ref annotations, RelationalAnnotationNames.DefaultValueSql); RemoveAnnotation(ref annotations, RelationalAnnotationNames.Comment); RemoveAnnotation(ref annotations, RelationalAnnotationNames.ComputedColumnSql); RemoveAnnotation(ref annotations, RelationalAnnotationNames.IsFixedLength); + RemoveAnnotation(ref annotations, RelationalAnnotationNames.TableColumnMappings); RemoveAnnotation(ref annotations, ScaffoldingAnnotationNames.ColumnOrdinal); if (!useDataAnnotations) diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs index 45c32cf6d4c..4f642c24cdc 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs @@ -33,6 +33,11 @@ public static string GetTableName([NotNull] this IEntityType entityType) => /// The default name of the table to which the entity type would be mapped. public static string GetDefaultTableName([NotNull] this IEntityType entityType) { + if (entityType.GetDefiningQuery() != null) + { + return null; + } + var ownership = entityType.FindOwnership(); if (ownership != null && ownership.IsUnique) @@ -140,6 +145,41 @@ public static void SetSchema( => entityType.FindAnnotation(RelationalAnnotationNames.Schema) ?.GetConfigurationSource(); + /// + /// Returns the name of the table to which the entity type is mapped. + /// + /// The entity type to get the table name for. + /// The name of the table to which the entity type is mapped. + public static IEnumerable GetTableMappings([NotNull] this IEntityType entityType) => + (IEnumerable)entityType[RelationalAnnotationNames.TableMappings]; + + /// + /// Returns the name of the view to which the entity type is mapped. + /// + /// The entity type to get the view name for. + /// The name of the view to which the entity type is mapped. + public static string GetViewName([NotNull] this IEntityType entityType) + { + if (entityType.BaseType != null) + { + return entityType.GetRootType().GetViewName(); + } + + if (entityType.FindAnnotation(RelationalAnnotationNames.ViewDefinition) != null) + { + return entityType.GetTableName(); + } + + var ownership = entityType.FindOwnership(); + if (ownership != null + && ownership.IsUnique) + { + return ownership.PrincipalEntityType.GetViewName(); + } + + return null; + } + /// /// Finds an with the given name. /// diff --git a/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs b/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs index 113cd591cdd..ac185a02bab 100644 --- a/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs @@ -55,6 +55,26 @@ public static void SetDefaultSchema( public static ConfigurationSource? GetDefaultSchemaConfigurationSource([NotNull] this IConventionModel model) => model.FindAnnotation(RelationalAnnotationNames.DefaultSchema)?.GetConfigurationSource(); + /// + /// Returns all the tables mapped in the model. + /// + /// The model to get the tables for. + /// All the tables mapped in the model. + public static IEnumerable GetTables([NotNull] this IModel model) => + ((IDictionary<(string, string), Table>)model[RelationalAnnotationNames.Tables]).Values; + + /// + /// Gets the table with a given name. Returns null if no table with the given name is defined. + /// + /// The model to get the table for. + /// The name of the table. + /// The schema of the table. + /// The table with a given name or null if no table with the given name is defined. + public static ITable FindTable([NotNull] this IModel model, [NotNull] string name, [CanBeNull] string schema) => + ((IDictionary<(string, string), Table>)model[RelationalAnnotationNames.Tables]).TryGetValue((name, schema), out var table) + ? table + : null; + /// /// Returns the maximum length allowed for store identifiers. /// diff --git a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs index 3f90ff0f311..e7fdbed9324 100644 --- a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs @@ -174,6 +174,14 @@ public static void SetColumnType( public static ConfigurationSource? GetColumnTypeConfigurationSource([NotNull] this IConventionProperty property) => property.FindAnnotation(RelationalAnnotationNames.ColumnType)?.GetConfigurationSource(); + /// + /// Returns the columns to which the property is mapped. + /// + /// The property. + /// The name of the table to which the entity type is mapped. + public static IEnumerable GetTableColumnMappings([NotNull] this IProperty property) => + (IEnumerable)property[RelationalAnnotationNames.TableColumnMappings]; + /// /// Returns the SQL expression that is used as the default value for the column this property is mapped to. /// diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index d00c17a598b..7652ba70318 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs @@ -156,7 +156,13 @@ protected virtual void ValidateSharedTableCompatibility( var tables = new Dictionary>(); foreach (var entityType in model.GetEntityTypes()) { - var tableName = Format(entityType.GetSchema(), entityType.GetTableName()); + var name = entityType.GetTableName(); + if (name == null) + { + continue; + } + + var tableName = Format(entityType.GetSchema(), name); if (!tables.TryGetValue(tableName, out var mappedTypes)) { diff --git a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs index 1d7fc3d0fb7..1ebbec7bed3 100644 --- a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs +++ b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs @@ -97,6 +97,11 @@ public override ConventionSet CreateConventionSet() (QueryFilterDefiningQueryRewritingConvention)new RelationalQueryFilterDefiningQueryRewritingConvention( Dependencies, RelationalDependencies)); + ConventionSet.AddAfter( + conventionSet.ModelFinalizedConventions, + new RelationalModelConvention(), + typeof(ValidatingConvention)); + return conventionSet; } } diff --git a/src/EFCore.Relational/Metadata/Conventions/RelationalModelConvention.cs b/src/EFCore.Relational/Metadata/Conventions/RelationalModelConvention.cs new file mode 100644 index 00000000000..6d009a35914 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Conventions/RelationalModelConvention.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions +{ + /// + /// A convention that precomputes a relational model. + /// + public class RelationalModelConvention : IModelFinalizedConvention + { + /// + public virtual IModel ProcessModelFinalized(IModel model) + => model is IConventionModel conventionModel ? RelationalModel.AddRelationalModel(conventionModel) : model; + } +} diff --git a/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs b/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs index a8713826196..108e30b3d90 100644 --- a/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs @@ -69,6 +69,10 @@ private static void TryUniquifyTableNames( foreach (var entityType in model.GetEntityTypes()) { var tableName = (Schema: entityType.GetSchema(), TableName: entityType.GetTableName()); + if (tableName.TableName == null) + { + continue; + } if (!tables.TryGetValue(tableName, out var entityTypes)) { diff --git a/src/EFCore.Relational/Metadata/IColumn.cs b/src/EFCore.Relational/Metadata/IColumn.cs new file mode 100644 index 00000000000..8211ef93efd --- /dev/null +++ b/src/EFCore.Relational/Metadata/IColumn.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + /// + /// Represents a column in a table. + /// + public interface IColumn : IColumnBase + { + /// + /// The containing table. + /// + new ITable Table { get; } + + /// + /// The property mappings. + /// + new IEnumerable PropertyMappings { get; } + } +} diff --git a/src/EFCore.Relational/Metadata/IColumnBase.cs b/src/EFCore.Relational/Metadata/IColumnBase.cs new file mode 100644 index 00000000000..827c96f6225 --- /dev/null +++ b/src/EFCore.Relational/Metadata/IColumnBase.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + /// + /// Represents a column-like object in a table-like object. + /// + public interface IColumnBase : IAnnotatable + { + /// + /// The column name. + /// + string Name { get; } + + /// + /// The column type. + /// + string Type { get; } + + /// + /// Whether the column can contain NULL. + /// + bool IsNullable { get; } + + /// + /// The containing table-like object. + /// + ITableBase Table { get; } + + /// + /// The property mappings. + /// + IEnumerable PropertyMappings { get; } + } +} diff --git a/src/EFCore.Relational/Metadata/IColumnMapping.cs b/src/EFCore.Relational/Metadata/IColumnMapping.cs new file mode 100644 index 00000000000..7338ab1c206 --- /dev/null +++ b/src/EFCore.Relational/Metadata/IColumnMapping.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + /// + /// Represents property mapping to a column. + /// + public interface IColumnMapping : IColumnMappingBase + { + /// + /// The target column. + /// + new IColumn Column { get; } + + /// + /// The containing table mapping. + /// + new ITableMapping TableMapping { get; } + } +} diff --git a/src/EFCore.Relational/Metadata/IColumnMappingBase.cs b/src/EFCore.Relational/Metadata/IColumnMappingBase.cs new file mode 100644 index 00000000000..d5a021a501d --- /dev/null +++ b/src/EFCore.Relational/Metadata/IColumnMappingBase.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + /// + /// Represents property mapping to a column-like object. + /// + public interface IColumnMappingBase : IAnnotatable + { + /// + /// The mapped property. + /// + IProperty Property { get; } + + /// + /// The target column-like object. + /// + IColumnBase Column { get; } + + /// + /// The type mapping for the column-like object. + /// + RelationalTypeMapping TypeMapping { get; } + + /// + /// The containing table mapping. + /// + ITableMappingBase TableMapping { get; } + } +} diff --git a/src/EFCore.Relational/Metadata/ITable.cs b/src/EFCore.Relational/Metadata/ITable.cs new file mode 100644 index 00000000000..da5d207e5d3 --- /dev/null +++ b/src/EFCore.Relational/Metadata/ITable.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + /// + /// Represents a table in the database. + /// + public interface ITable : ITableBase + { + /// + /// The entity type mappings. + /// + new IEnumerable EntityTypeMappings { get; } + + /// + /// The columns defined for this table. + /// + new IEnumerable Columns { get; } + + /// + /// Indicates whether the table should be managed by migrations + /// + bool IsMigratable { get; } + + /// + /// Returns a value indicating whether multiple entity types are sharing the rows in the table. + /// + bool IsSplit { get; } + + /// + /// Returns the column with a given name. Returns null if no column with the given name is defined. + /// + IColumn FindColumn([NotNull] string name); + + /// + /// Returns the foreign keys for the given entity type that point to other entity types sharing this table. + /// + IEnumerable GetInternalForeignKeys([NotNull] IEntityType entityType); + + /// + /// Returns the foreign keys referencing the given entity type from other entity types sharing this table. + /// + IEnumerable GetReferencingInternalForeignKeys([NotNull] IEntityType entityType); + + /// + /// Returns the check constraints for this table. + /// + IEnumerable GetCheckConstraints() + => EntityTypeMappings.SelectMany(m => CheckConstraint.GetCheckConstraints(m.EntityType)) + .Distinct((x, y) => x.Name == y.Name); + + /// + /// Returns the comment for this table. + /// + public virtual string GetComment() + => EntityTypeMappings.Select(e => e.EntityType.GetComment()).FirstOrDefault(c => c != null); + } +} diff --git a/src/EFCore.Relational/Metadata/ITableBase.cs b/src/EFCore.Relational/Metadata/ITableBase.cs new file mode 100644 index 00000000000..11d1b6ec83b --- /dev/null +++ b/src/EFCore.Relational/Metadata/ITableBase.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + /// + /// Represents a table-like object in the database. + /// + public interface ITableBase : IAnnotatable + { + /// + /// The name of the table in the database. + /// + string Name { get; } + + /// + /// The schema of the table in the database. + /// + string Schema { get; } + + /// + /// The entity type mappings. + /// + IEnumerable EntityTypeMappings { get; } + + /// + /// The columns defined for this table. + /// + IEnumerable Columns { get; } + } +} diff --git a/src/EFCore.Relational/Metadata/ITableMapping.cs b/src/EFCore.Relational/Metadata/ITableMapping.cs new file mode 100644 index 00000000000..e44bbbbf2c5 --- /dev/null +++ b/src/EFCore.Relational/Metadata/ITableMapping.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + /// + /// Represents entity type mapping to a table. + /// + public interface ITableMapping : ITableMappingBase + { + /// + /// The target table. + /// + new ITable Table { get; } + + /// + /// The properties mapped to columns on the target table. + /// + new IEnumerable ColumnMappings { get; } + } +} diff --git a/src/EFCore.Relational/Metadata/ITableMappingBase.cs b/src/EFCore.Relational/Metadata/ITableMappingBase.cs new file mode 100644 index 00000000000..7bdd09344d3 --- /dev/null +++ b/src/EFCore.Relational/Metadata/ITableMappingBase.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + /// + /// Represents entity type mapping to a table-like object. + /// + public interface ITableMappingBase : IAnnotatable + { + /// + /// The mapped entity type. + /// + IEntityType EntityType { get; } + + /// + /// The target table-like object. + /// + ITableBase Table { get; } + + /// + /// The properties mapped to columns on the target table. + /// + IEnumerable ColumnMappings { get; } + + /// + /// Indicates whether the inherited properties use the same mapping. + /// + bool IncludesDerivedTypes { get; } + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/Column.cs b/src/EFCore.Relational/Metadata/Internal/Column.cs new file mode 100644 index 00000000000..fef8726a01a --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/Column.cs @@ -0,0 +1,81 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public class Column : Annotatable, IColumn + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public Column([NotNull] string name, [NotNull] string type, [NotNull] ITable table) + { + Name = name; + Type = type; + Table = table; + } + + /// + public virtual string Name { get; } + + /// + public virtual ITable Table { get; } + + /// + public virtual string Type { get; } + + /// + public virtual bool IsNullable { get; set; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual SortedSet PropertyMappings { get; } = new SortedSet(ColumnMappingComparer.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 override string ToString() => this.ToDebugString(MetadataDebugStringOptions.SingleLineDefault); + + /// + IEnumerable IColumn.PropertyMappings + { + [DebuggerStepThrough] + get => PropertyMappings; + } + + /// + IEnumerable IColumnBase.PropertyMappings + { + [DebuggerStepThrough] + get => PropertyMappings; + } + + /// + ITableBase IColumnBase.Table + { + [DebuggerStepThrough] + get => Table; + } + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/ColumnExtensions.cs b/src/EFCore.Relational/Metadata/Internal/ColumnExtensions.cs new file mode 100644 index 00000000000..e8b99b83267 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/ColumnExtensions.cs @@ -0,0 +1,63 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Text; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static class ColumnExtensions + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static string ToDebugString( + [NotNull] this IColumn column, + MetadataDebugStringOptions options, + [NotNull] string indent = "") + { + var builder = new StringBuilder(); + + builder.Append(indent); + + var singleLine = (options & MetadataDebugStringOptions.SingleLine) != 0; + if (singleLine) + { + builder.Append($"Column: {column.Table.Name}."); + } + + builder.Append(column.Name).Append(" ("); + + builder.Append(column.Type).Append(")"); + + if (column.IsNullable) + { + builder.Append(" Nullable"); + } + else + { + builder.Append(" NonNullable"); + } + + builder.Append(")"); + + if (!singleLine && + (options & MetadataDebugStringOptions.IncludeAnnotations) != 0) + { + builder.Append(column.AnnotationsToDebugString(indent + " ")); + } + + return builder.ToString(); + } + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/ColumnMapping.cs b/src/EFCore.Relational/Metadata/Internal/ColumnMapping.cs new file mode 100644 index 00000000000..22103cd8b3e --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/ColumnMapping.cs @@ -0,0 +1,71 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Diagnostics; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public class ColumnMapping : Annotatable, IColumnMapping + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public ColumnMapping( + [NotNull] IProperty property, + [NotNull] IColumn column, + [NotNull] RelationalTypeMapping typeMapping, + [NotNull] ITableMapping tableMapping) + { + Property = property; + Column = column; + TypeMapping = typeMapping; + TableMapping = tableMapping; + } + + /// + public virtual IProperty Property { get; } + + /// + public virtual IColumn Column { get; } + + /// + public virtual RelationalTypeMapping TypeMapping { get; } + + /// + public virtual ITableMapping TableMapping { 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 override string ToString() => this.ToDebugString(MetadataDebugStringOptions.SingleLineDefault); + + /// + IColumnBase IColumnMappingBase.Column + { + [DebuggerStepThrough] + get => Column; + } + + /// + ITableMappingBase IColumnMappingBase.TableMapping + { + [DebuggerStepThrough] + get => TableMapping; + } + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/ColumnMappingComparer.cs b/src/EFCore.Relational/Metadata/Internal/ColumnMappingComparer.cs new file mode 100644 index 00000000000..02ee63f5a0a --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/ColumnMappingComparer.cs @@ -0,0 +1,76 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public class ColumnMappingComparer : IEqualityComparer, IComparer + { + private ColumnMappingComparer() + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static readonly ColumnMappingComparer Instance = new ColumnMappingComparer(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your 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 int Compare(IColumnMapping x, IColumnMapping y) + { + var result = StringComparer.Ordinal.Compare(x.Property.IsColumnNullable(), y.Property.IsColumnNullable()); + if (result != 0) + { + return result; + } + + result = StringComparer.Ordinal.Compare(x.Property.Name, y.Property.Name); + if (result != 0) + { + return result; + } + + return StringComparer.Ordinal.Compare(x.Column.Name, y.Column.Name); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool Equals(IColumnMapping x, IColumnMapping y) + => x.Property == y.Property + && x.Column == y.Column; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual int GetHashCode(IColumnMapping obj) + { + var hashCode = new HashCode(); + hashCode.Add(obj.Property.Name); + hashCode.Add(obj.Column.Name); + return hashCode.ToHashCode(); + } + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/ColumnMappingExtensions.cs b/src/EFCore.Relational/Metadata/Internal/ColumnMappingExtensions.cs new file mode 100644 index 00000000000..7a5a058e9d7 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/ColumnMappingExtensions.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Text; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static class ColumnMappingExtensions + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static string ToDebugString( + [NotNull] this IColumnMapping columnMapping, + MetadataDebugStringOptions options, + [NotNull] string indent = "") + { + var builder = new StringBuilder(); + + builder.Append(indent); + + var singleLine = (options & MetadataDebugStringOptions.SingleLine) != 0; + if (singleLine) + { + builder.Append($"ColumnMapping: "); + } + + builder.Append(columnMapping.Property.Name).Append(" - "); + + builder.Append(columnMapping.Column.Name); + + if (!singleLine && + (options & MetadataDebugStringOptions.IncludeAnnotations) != 0) + { + builder.Append(columnMapping.AnnotationsToDebugString(indent + " ")); + } + + return builder.ToString(); + } + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs new file mode 100644 index 00000000000..90f2a5a7a3b --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs @@ -0,0 +1,159 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public class RelationalModel + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your 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 IModel AddRelationalModel([NotNull] IConventionModel model) + { + var tables = new SortedDictionary<(string, string), Table>(); + foreach (var entityType in model.GetEntityTypes()) + { + var tableName = entityType.GetTableName(); + if (tableName == null + || entityType.GetViewName() != null) + { + continue; + } + + var schema = entityType.GetSchema(); + if (!tables.TryGetValue((tableName, schema), out var table)) + { + table = new Table(tableName, schema); + tables.Add((tableName, schema), table); + } + + table.IsMigratable = true; + + var tableMapping = new TableMapping(entityType, table, includesDerivedTypes: true); + foreach (var property in entityType.GetDeclaredProperties()) + { + var typeMapping = property.GetRelationalTypeMapping(); + var columnName = property.GetColumnName(); + var column = table.FindColumn(columnName) as Column; + if (column == null) + { + column = new Column(columnName, property.GetColumnType() ?? typeMapping.StoreType, table); + column.IsNullable = property.IsColumnNullable(); + table.Columns.Add(columnName, column); + } + else if (!property.IsColumnNullable()) + { + column.IsNullable = false; + } + + var columnMapping = new ColumnMapping(property, column, typeMapping, tableMapping); + tableMapping.ColumnMappings.Add(columnMapping); + column.PropertyMappings.Add(columnMapping); + + var columnMappings = property[RelationalAnnotationNames.TableColumnMappings] as SortedSet; + if (columnMappings == null) + { + columnMappings = new SortedSet(ColumnMappingComparer.Instance); + property.SetAnnotation(RelationalAnnotationNames.TableColumnMappings, columnMappings); + } + + columnMappings.Add(columnMapping); + } + + var tableMappings = entityType[RelationalAnnotationNames.TableMappings] as SortedSet; + if (tableMappings == null) + { + tableMappings = new SortedSet(TableMappingComparer.Instance); + entityType.SetAnnotation(RelationalAnnotationNames.TableMappings, tableMappings); + } + + tableMappings.Add(tableMapping); + table.EntityTypeMappings.Add(tableMapping); + } + + foreach (var table in tables.Values) + { + SortedDictionary> internalForeignKeyMap = null; + SortedDictionary> referencingInternalForeignKeyMap = null; + foreach (var entityTypeMapping in table.EntityTypeMappings) + { + var entityType = entityTypeMapping.EntityType; + var primaryKey = entityType.FindPrimaryKey(); + if (primaryKey == null) + { + continue; + } + + SortedSet internalForeignKeys = null; + foreach (var foreignKey in entityType.FindForeignKeys(primaryKey.Properties)) + { + if (foreignKey.IsUnique + && foreignKey.PrincipalKey.IsPrimaryKey() + && !foreignKey.IsIntraHierarchical() + && table.EntityTypeMappings.Any(m => m.EntityType == foreignKey.PrincipalEntityType)) + { + if (internalForeignKeys == null) + { + internalForeignKeys = new SortedSet(ForeignKeyComparer.Instance); + } + internalForeignKeys.Add(foreignKey); + + if (referencingInternalForeignKeyMap == null) + { + referencingInternalForeignKeyMap = + new SortedDictionary>(EntityTypePathComparer.Instance); + } + + var principalEntityType = foreignKey.PrincipalEntityType; + if (!referencingInternalForeignKeyMap.TryGetValue(principalEntityType, out var internalReferencingForeignKeys)) + { + internalReferencingForeignKeys = new SortedSet(ForeignKeyComparer.Instance); + referencingInternalForeignKeyMap[principalEntityType] = internalReferencingForeignKeys; + } + ((SortedSet)internalReferencingForeignKeys).Add(foreignKey); + } + } + + if (internalForeignKeys != null) + { + if (internalForeignKeyMap == null) + { + internalForeignKeyMap = + new SortedDictionary>(EntityTypePathComparer.Instance); + table.InternalForeignKeys = internalForeignKeyMap; + } + + internalForeignKeyMap[entityType] = internalForeignKeys; + } + + if (internalForeignKeys == null + && table.EntityTypeMappings.Any(m => !m.EntityType.IsSameHierarchy(entityType))) + { + table.IsSplit = true; + } + } + + if (referencingInternalForeignKeyMap != null) + { + table.ReferencingInternalForeignKeys = referencingInternalForeignKeyMap; + } + } + + model.SetAnnotation(RelationalAnnotationNames.Tables, tables); + return model; + } + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalPropertyExtensions.cs b/src/EFCore.Relational/Metadata/Internal/RelationalPropertyExtensions.cs index 8a34db48f15..0ca625b3def 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalPropertyExtensions.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalPropertyExtensions.cs @@ -38,17 +38,16 @@ public static IForeignKey FindSharedTableLink([NotNull] this IProperty property, } var entityType = property.DeclaringEntityType; - foreach (var fk in entityType.FindForeignKeys(pk.Properties)) { if (!fk.PrincipalKey.IsPrimaryKey() - || fk.PrincipalEntityType == fk.DeclaringEntityType) + || fk.PrincipalEntityType == fk.DeclaringEntityType + || !fk.IsUnique) { continue; } var principalEntityType = fk.PrincipalEntityType; - var declaringEntityType = fk.DeclaringEntityType; if (table == principalEntityType.GetTableName() && schema == principalEntityType.GetSchema()) { diff --git a/src/EFCore.Relational/Metadata/Internal/Table.cs b/src/EFCore.Relational/Metadata/Internal/Table.cs new file mode 100644 index 00000000000..d5d076e50a1 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/Table.cs @@ -0,0 +1,130 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public class Table : Annotatable, ITable + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public Table([NotNull] string name, [CanBeNull] string schema) + { + Schema = schema; + Name = name; + } + + /// + public virtual string Schema { get; } + + /// + public virtual string Name { 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 SortedSet EntityTypeMappings { get; } = new SortedSet(TableMappingComparer.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 SortedDictionary Columns { get; } = new SortedDictionary(StringComparer.Ordinal); + + /// + public virtual bool IsMigratable { get; set; } + + /// + public virtual bool IsSplit { get; set; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual SortedDictionary> InternalForeignKeys { get; [param: NotNull] set; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual SortedDictionary> ReferencingInternalForeignKeys { get; [param: NotNull] set; } + + /// + public virtual IColumn FindColumn(string name) + => Columns.TryGetValue(name, out var column) + ? column + : 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 override string ToString() => this.ToDebugString(MetadataDebugStringOptions.SingleLineDefault); + + /// + IEnumerable ITableBase.Columns + { + [DebuggerStepThrough] + get => Columns.Values; + } + + /// + IEnumerable ITable.Columns + { + [DebuggerStepThrough] + get => Columns.Values; + } + + /// + IEnumerable ITable.EntityTypeMappings + { + [DebuggerStepThrough] + get => EntityTypeMappings; + } + + /// + IEnumerable ITableBase.EntityTypeMappings + { + [DebuggerStepThrough] + get => EntityTypeMappings; + } + + IEnumerable ITable.GetInternalForeignKeys(IEntityType entityType) + => InternalForeignKeys != null + && InternalForeignKeys.TryGetValue(entityType, out var foreignKeys) + ? foreignKeys + : null; + + IEnumerable ITable.GetReferencingInternalForeignKeys(IEntityType entityType) + => ReferencingInternalForeignKeys != null + && ReferencingInternalForeignKeys.TryGetValue(entityType, out var foreignKeys) + ? foreignKeys + : null; + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/TableExtensions.cs b/src/EFCore.Relational/Metadata/Internal/TableExtensions.cs new file mode 100644 index 00000000000..eafc78c8d81 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/TableExtensions.cs @@ -0,0 +1,81 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Text; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static class TableExtensions + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static string ToDebugString( + [NotNull] this ITable table, + MetadataDebugStringOptions options, + [NotNull] string indent = "") + { + var builder = new StringBuilder(); + + builder + .Append(indent) + .Append("Table: "); + + if (table.Schema != null) + { + builder + .Append(table.Schema) + .Append("."); + } + + builder.Append(table.Name); + + if (!table.IsMigratable) + { + builder.Append(" NonMigratable"); + } + + if ((options & MetadataDebugStringOptions.SingleLine) == 0) + { + var mappings = table.EntityTypeMappings.ToList(); + if (mappings.Count != 0) + { + builder.AppendLine().Append(indent).Append(" EntityTypeMappings: "); + foreach (var mapping in mappings) + { + builder.AppendLine().Append(mapping.ToDebugString(options, indent + " ")); + } + } + + var columns = table.Columns.ToList(); + if (columns.Count != 0) + { + builder.AppendLine().Append(indent).Append(" Properties: "); + foreach (var column in columns) + { + builder.AppendLine().Append(column.ToDebugString(options, indent + " ")); + } + } + + if ((options & MetadataDebugStringOptions.IncludeAnnotations) != 0) + { + builder.Append(table.AnnotationsToDebugString(indent: indent + " ")); + } + } + + return builder.ToString(); + } + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/TableMapping.cs b/src/EFCore.Relational/Metadata/Internal/TableMapping.cs index ac273d94965..ab4b3255b4b 100644 --- a/src/EFCore.Relational/Metadata/Internal/TableMapping.cs +++ b/src/EFCore.Relational/Metadata/Internal/TableMapping.cs @@ -1,11 +1,10 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Infrastructure; namespace Microsoft.EntityFrameworkCore.Metadata.Internal { @@ -15,7 +14,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 + public class TableMapping : Annotatable, ITableMapping { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -24,30 +23,20 @@ public class TableMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public TableMapping( - [CanBeNull] string schema, - [NotNull] string name, - [NotNull] IReadOnlyList entityTypes) + [NotNull] IEntityType entityType, + [NotNull] ITable table, + bool includesDerivedTypes) { - Schema = schema; - Name = name; - EntityTypes = entityTypes; + EntityType = entityType; + Table = table; + IncludesDerivedTypes = includesDerivedTypes; } - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your 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 string Schema { get; } + /// + public virtual IEntityType EntityType { 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 string Name { get; } + /// + public virtual ITable Table { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -55,24 +44,10 @@ public TableMapping( /// any release. You should only use it directly in your 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 EntityTypes { get; } + public virtual SortedSet ColumnMappings { get; } = new SortedSet(ColumnMappingComparer.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 IEntityType GetRootType() - => EntityTypes.SingleOrDefault( - t => t.BaseType == null - && (t.FindDeclaredPrimaryKey() == null - || t.FindForeignKeys(t.FindDeclaredPrimaryKey().Properties) - .All( - fk => !fk.PrincipalKey.IsPrimaryKey() - || fk.PrincipalEntityType.GetRootType() == t - || t.GetTableName() != fk.PrincipalEntityType.GetTableName() - || t.GetSchema() != fk.PrincipalEntityType.GetSchema()))); + /// + public virtual bool IncludesDerivedTypes { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -80,130 +55,27 @@ public virtual IEntityType GetRootType() /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual IEnumerable GetProperties() => GetPropertyMap().Values; + public override string ToString() => this.ToDebugString(MetadataDebugStringOptions.SingleLineDefault); - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your 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 Dictionary GetPropertyMap() + /// + ITableBase ITableMappingBase.Table { - var dictionary = new Dictionary(StringComparer.Ordinal); - foreach (var property in EntityTypes.SelectMany(EntityFrameworkCore.EntityTypeExtensions.GetDeclaredProperties)) - { - var columnName = property.GetColumnName(); - if (!dictionary.TryGetValue(columnName, out var otherProperty) - || (otherProperty.IsColumnNullable() && !property.IsColumnNullable())) - { - dictionary[columnName] = property; - } - } - - return dictionary; + [DebuggerStepThrough] + get => 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 IEnumerable GetKeys() - => EntityTypes.SelectMany(EntityFrameworkCore.EntityTypeExtensions.GetDeclaredKeys) - .Distinct((x, y) => x.GetName() == y.GetName()); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual IEnumerable GetIndexes() - => EntityTypes.SelectMany(EntityFrameworkCore.EntityTypeExtensions.GetDeclaredIndexes) - .Distinct((x, y) => x.GetName() == y.GetName()); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual IEnumerable GetForeignKeys() - => EntityTypes.SelectMany(EntityFrameworkCore.EntityTypeExtensions.GetDeclaredForeignKeys) - .Distinct((x, y) => x.GetConstraintName() == y.GetConstraintName()) - .Where( - fk => !(EntityTypes.Contains(fk.PrincipalEntityType) - && fk.Properties.Select(p => p.GetColumnName()) - .SequenceEqual(fk.PrincipalKey.Properties.Select(p => p.GetColumnName())))); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual IEnumerable GetCheckConstraints() - => EntityTypes.SelectMany(CheckConstraint.GetCheckConstraints) - .Distinct((x, y) => x.Name == y.Name); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public static IReadOnlyList GetTableMappings([NotNull] IModel model) + /// + IEnumerable ITableMapping.ColumnMappings { - var tables = new Dictionary<(string Schema, string TableName), List>(); - foreach (var entityType in model.GetEntityTypes().Where(et => !et.IsIgnoredByMigrations())) - { - var fullName = (entityType.GetSchema(), entityType.GetTableName()); - if (!tables.TryGetValue(fullName, out var mappedEntityTypes)) - { - mappedEntityTypes = new List(); - tables.Add(fullName, mappedEntityTypes); - } - - // TODO: Consider sorting to keep hierarchies together - mappedEntityTypes.Add(entityType); - } - - return tables.Select(kv => new TableMapping(kv.Key.Schema, kv.Key.TableName, kv.Value)) - .OrderBy(t => t.Schema).ThenBy(t => t.Name).ToList(); + [DebuggerStepThrough] + get => ColumnMappings; } - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your 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 TableMapping GetTableMapping([NotNull] IModel model, [NotNull] string table, [CanBeNull] string schema) + /// + IEnumerable ITableMappingBase.ColumnMappings { - var mappedEntities = new List(); - foreach (var entityType in model.GetEntityTypes().Where(et => et.FindPrimaryKey() != null)) - { - if (table == entityType.GetTableName() - && schema == entityType.GetSchema()) - { - mappedEntities.Add(entityType); - } - } - - return mappedEntities.Count > 0 - ? new TableMapping(schema, table, mappedEntities) - : null; + [DebuggerStepThrough] + get => ColumnMappings; } - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your 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 string GetComment() - => EntityTypes.Select(e => e.GetComment()).FirstOrDefault(c => c != null); } } diff --git a/src/EFCore.Relational/Metadata/Internal/TableMappingComparer.cs b/src/EFCore.Relational/Metadata/Internal/TableMappingComparer.cs new file mode 100644 index 00000000000..0d9933c7aea --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/TableMappingComparer.cs @@ -0,0 +1,105 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public class TableMappingComparer : IEqualityComparer, IComparer + { + private TableMappingComparer() + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static readonly TableMappingComparer Instance = new TableMappingComparer(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your 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 int Compare(ITableMapping x, ITableMapping y) + { + var result = EntityTypePathComparer.Instance.Compare(x.EntityType, y.EntityType); + if (result != 0) + { + return result; + } + + result = StringComparer.Ordinal.Compare(x.Table.Name, y.Table.Name); + if (result != 0) + { + return result; + } + + result = StringComparer.Ordinal.Compare(x.Table.Schema, y.Table.Schema); + if (result != 0) + { + return result; + } + + result = x.IncludesDerivedTypes.CompareTo(y.IncludesDerivedTypes); + if (result != 0) + { + return result; + } + + result = x.ColumnMappings.Count().CompareTo(y.ColumnMappings.Count()); + if (result != 0) + { + return result; + } + + return x.ColumnMappings.Zip(y.ColumnMappings, (xc, yc) => ColumnMappingComparer.Instance.Compare(xc, yc)) + .FirstOrDefault(r => r != 0); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool Equals(ITableMapping x, ITableMapping y) + => x.EntityType == y.EntityType + && x.Table == y.Table + && x.IncludesDerivedTypes == y.IncludesDerivedTypes + && StructuralComparisons.StructuralEqualityComparer.Equals(x.ColumnMappings, y.ColumnMappings); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your 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 int GetHashCode(ITableMapping obj) + { + var hashCode = new HashCode(); + hashCode.Add(obj.EntityType, EntityTypePathComparer.Instance); + hashCode.Add(obj.Table.Name); + hashCode.Add(obj.Table.Schema); + foreach (var columnMapping in obj.ColumnMappings) + { + hashCode.Add(columnMapping, ColumnMappingComparer.Instance); + } + hashCode.Add(obj.IncludesDerivedTypes); + return hashCode.ToHashCode(); + } + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/TableMappingExtensions.cs b/src/EFCore.Relational/Metadata/Internal/TableMappingExtensions.cs new file mode 100644 index 00000000000..6810b25efa1 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/TableMappingExtensions.cs @@ -0,0 +1,57 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Text; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static class TableMappingExtensions + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static string ToDebugString( + [NotNull] this ITableMapping tableMapping, + MetadataDebugStringOptions options, + [NotNull] string indent = "") + { + var builder = new StringBuilder(); + + builder.Append(indent); + + var singleLine = (options & MetadataDebugStringOptions.SingleLine) != 0; + if (singleLine) + { + builder.Append($"TableMapping: "); + } + + builder.Append(tableMapping.EntityType.Name).Append(" - "); + + builder.Append(tableMapping.Table.Name); + + if (tableMapping.IncludesDerivedTypes) + { + builder.Append($" IncludesDerivedTypes"); + } + + if (!singleLine && + (options & MetadataDebugStringOptions.IncludeAnnotations) != 0) + { + builder.Append(tableMapping.AnnotationsToDebugString(indent + " ")); + } + + return builder.ToString(); + } + } +} diff --git a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs index fafdb4f054c..7072d918d23 100644 --- a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs +++ b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs @@ -10,7 +10,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata public static class RelationalAnnotationNames { /// - /// The prefix used for any relational annotation. + /// The prefix used for all relational annotations. /// public const string Prefix = "Relational:"; @@ -85,18 +85,33 @@ public static class RelationalAnnotationNames public const string DbFunction = Prefix + "DbFunction"; /// - /// The maximum length for database identifiers. + /// The name for the annotation containing the maximum length for database identifiers. /// public const string MaxIdentifierLength = Prefix + "MaxIdentifierLength"; /// - /// A flag indicating whether the property is constrained to fixed length values. + /// The name for the annotation containing a flag indicating whether the property is constrained to fixed length values. /// public const string IsFixedLength = Prefix + "IsFixedLength"; /// - /// The definition of a database view. + /// The name for the annotation containing the definition of a database view. /// public const string ViewDefinition = Prefix + "ViewDefinition"; + + /// + /// The name for tables annotation. + /// + public const string Tables = Prefix + "Tables"; + + /// + /// The name for table mappings annotations. + /// + public const string TableMappings = Prefix + "TableMappings"; + + /// + /// The name for column mappings annotations. + /// + public const string TableColumnMappings = Prefix + "TableColumnMappings"; } } diff --git a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs index 91ff8c53eb1..ead09f2a687 100644 --- a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs +++ b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs @@ -314,7 +314,7 @@ protected virtual IReadOnlyList Sort( foreach (var dropTableOperation in dropTableOperations) { var table = diffContext.FindTable(dropTableOperation); - foreach (var foreignKey in table.GetForeignKeys()) + foreach (var foreignKey in GetForeignKeys(table)) { var principalRootEntityType = foreignKey.PrincipalEntityType; var principalDropTableOperation = diffContext.FindDrop(principalRootEntityType); @@ -380,8 +380,8 @@ protected virtual IEnumerable Diff( .Concat(Diff(source.GetSequences(), target.GetSequences(), diffContext)) .Concat( Diff( - diffContext.GetSourceTables().SelectMany(s => s.GetForeignKeys()), - diffContext.GetTargetTables().SelectMany(t => t.GetForeignKeys()), + diffContext.GetSourceTables().SelectMany(s => GetForeignKeys(s)), + diffContext.GetTargetTables().SelectMany(t => GetForeignKeys(t)), diffContext)) : target != null ? Add(target, diffContext) @@ -444,7 +444,7 @@ protected virtual IEnumerable Add([NotNull] IModel target, [ .Concat(GetSchemas(target).SelectMany(t => Add(t, diffContext))) .Concat(diffContext.GetTargetTables().SelectMany(t => Add(t, diffContext))) .Concat(target.GetSequences().SelectMany(t => Add(t, diffContext))) - .Concat(diffContext.GetTargetTables().SelectMany(t => t.GetForeignKeys()).SelectMany(k => Add(k, diffContext))); + .Concat(diffContext.GetTargetTables().SelectMany(t => GetForeignKeys(t)).SelectMany(k => Add(k, diffContext))); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -519,8 +519,8 @@ protected virtual IEnumerable Remove([NotNull] string source /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected virtual IEnumerable Diff( - [NotNull] IEnumerable source, - [NotNull] IEnumerable target, + [NotNull] IEnumerable source, + [NotNull] IEnumerable target, [NotNull] DiffContext diffContext) => DiffCollection( source, @@ -541,11 +541,11 @@ protected virtual IEnumerable Diff( s.Name, t.Name, StringComparison.OrdinalIgnoreCase), - (s, t, c) => string.Equals(s.GetRootType().Name, t.GetRootType().Name, StringComparison.OrdinalIgnoreCase), - (s, t, c) => s.EntityTypes.Any( - se => t.EntityTypes.Any( + (s, t, c) => string.Equals(GetRootType(s).Name, GetRootType(t).Name, StringComparison.OrdinalIgnoreCase), + (s, t, c) => s.EntityTypeMappings.Any( + se => t.EntityTypeMappings.Any( te => - string.Equals(se.Name, te.Name, StringComparison.OrdinalIgnoreCase)))); + string.Equals(se.EntityType.Name, te.EntityType.Name, StringComparison.OrdinalIgnoreCase)))); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -554,10 +554,16 @@ protected virtual IEnumerable Diff( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected virtual IEnumerable Diff( - [NotNull] TableMapping source, - [NotNull] TableMapping target, + [NotNull] ITable source, + [NotNull] ITable target, [NotNull] DiffContext diffContext) { + if (!source.IsMigratable + || !target.IsMigratable) + { + yield break; + } + if (source.Schema != target.Schema || source.Name != target.Name) { @@ -571,8 +577,8 @@ protected virtual IEnumerable Diff( } // Validation should ensure that all the relevant annotations for the collocated entity types are the same - var sourceMigrationsAnnotations = MigrationsAnnotations.For(source.EntityTypes[0]).ToList(); - var targetMigrationsAnnotations = MigrationsAnnotations.For(target.EntityTypes[0]).ToList(); + var sourceMigrationsAnnotations = MigrationsAnnotations.For(GetRootType(source)).ToList(); + var targetMigrationsAnnotations = MigrationsAnnotations.For(GetRootType(target)).ToList(); if (source.GetComment() != target.GetComment() || HasDifferences(sourceMigrationsAnnotations, targetMigrationsAnnotations)) @@ -591,9 +597,10 @@ protected virtual IEnumerable Diff( yield return alterTableOperation; } - var operations = Diff(source.GetProperties(), target.GetProperties(), diffContext) - .Concat(Diff(source.GetKeys(), target.GetKeys(), diffContext)) - .Concat(Diff(source.GetIndexes(), target.GetIndexes(), diffContext)) + var operations = Diff(source.Columns.Select(c => c.PropertyMappings.First().Property), + target.Columns.Select(c => c.PropertyMappings.First().Property), diffContext) + .Concat(Diff(GetKeys(source), GetKeys(target), diffContext)) + .Concat(Diff(GetIndexes(source), GetIndexes(target), diffContext)) .Concat(Diff(source.GetCheckConstraints(), target.GetCheckConstraints(), diffContext)); foreach (var operation in operations) @@ -611,9 +618,14 @@ protected virtual IEnumerable Diff( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected virtual IEnumerable Add( - [NotNull] TableMapping target, [NotNull] DiffContext diffContext) + [NotNull] ITable target, [NotNull] DiffContext diffContext) { - var entityType = target.EntityTypes[0]; + if (!target.IsMigratable) + { + yield break; + } + + var entityType = GetRootType(target); var createTableOperation = new CreateTableOperation { Schema = target.Schema, @@ -624,27 +636,27 @@ protected virtual IEnumerable Add( createTableOperation.Columns.AddRange( GetSortedProperties(target).SelectMany(p => Add(p, diffContext, inline: true)).Cast()); - var primaryKey = target.EntityTypes[0].FindPrimaryKey(); + var primaryKey = entityType.FindPrimaryKey(); if (primaryKey != null) { createTableOperation.PrimaryKey = Add(primaryKey, diffContext).Cast().Single(); } createTableOperation.UniqueConstraints.AddRange( - target.GetKeys().Where(k => !k.IsPrimaryKey()).SelectMany(k => Add(k, diffContext)) + GetKeys(target).Where(k => !k.IsPrimaryKey()).SelectMany(k => Add(k, diffContext)) .Cast()); createTableOperation.CheckConstraints.AddRange( target.GetCheckConstraints().SelectMany(c => Add(c, diffContext)) .Cast()); - foreach (var targetEntityType in target.EntityTypes) + foreach (var targetMapping in target.EntityTypeMappings) { - diffContext.AddCreate(targetEntityType, createTableOperation); + diffContext.AddCreate(targetMapping.EntityType, createTableOperation); } yield return createTableOperation; - foreach (var operation in target.GetIndexes().SelectMany(i => Add(i, diffContext))) + foreach (var operation in GetIndexes(target).SelectMany(i => Add(i, diffContext))) { yield return operation; } @@ -657,18 +669,23 @@ protected virtual IEnumerable Add( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected virtual IEnumerable Remove( - [NotNull] TableMapping source, [NotNull] DiffContext diffContext) + [NotNull] ITable source, [NotNull] DiffContext diffContext) { + if (!source.IsMigratable) + { + yield break; + } + var operation = new DropTableOperation { Schema = source.Schema, Name = source.Name }; - operation.AddAnnotations(MigrationsAnnotations.ForRemove(source.EntityTypes[0])); + operation.AddAnnotations(MigrationsAnnotations.ForRemove(GetRootType(source))); diffContext.AddDrop(source, operation); yield return operation; } - private static IEnumerable GetSortedProperties(TableMapping target) - => GetSortedProperties(target.GetRootType()) + private static IEnumerable GetSortedProperties(ITable table) + => GetSortedProperties(GetRootType(table)) .Distinct((x, y) => x.GetColumnName() == y.GetColumnName()); private static IEnumerable GetSortedProperties(IEntityType entityType) @@ -862,11 +879,11 @@ private bool PropertyStructureEquals(IProperty source, IProperty target) private static bool EntityTypePathEquals(IEntityType source, IEntityType target, DiffContext diffContext) { - var sourceTable = diffContext.FindSourceTable(source); - var targetTable = diffContext.FindTargetTable(target); + var sourceTable = diffContext.GetTable(source); + var targetTable = diffContext.GetTable(target); - if (sourceTable.EntityTypes.Count == 1 - && targetTable.EntityTypes.Count == 1) + if (sourceTable.EntityTypeMappings.Count() == 1 + && targetTable.EntityTypeMappings.Count() == 1) { return true; } @@ -882,9 +899,9 @@ private static bool EntityTypePathEquals(IEntityType source, IEntityType target, var nextSource = source.DefiningEntityType ?? source.BaseType; var nextTarget = target.DefiningEntityType ?? target.BaseType; return nextSource == null - || !sourceTable.EntityTypes.Contains(nextSource) + || !sourceTable.EntityTypeMappings.Any(m => m.EntityType == nextSource) || nextTarget == null - || !targetTable.EntityTypes.Contains(nextTarget) + || !targetTable.EntityTypeMappings.Any(m => m.EntityType == nextTarget) || EntityTypePathEquals(nextSource, nextTarget, diffContext); } @@ -919,34 +936,28 @@ private static string GetDefiningNavigationName(IEntityType entityType) protected virtual IEnumerable Diff( [NotNull] IProperty source, [NotNull] IProperty target, [NotNull] DiffContext diffContext) { - var targetEntityType = target.DeclaringEntityType.GetRootType(); + var sourceMapping = source.GetTableColumnMappings().Single(); + var sourceColumn = sourceMapping.Column; + var targetMapping = target.GetTableColumnMappings().Single(); + var targetColumn = targetMapping.Column; + var table = targetColumn.Table; if (source.GetColumnName() != target.GetColumnName()) { yield return new RenameColumnOperation { - Schema = targetEntityType.GetSchema(), - Table = targetEntityType.GetTableName(), - Name = source.GetColumnName(), - NewName = target.GetColumnName() + Schema = table.Schema, + Table = table.Name, + Name = sourceColumn.Name, + NewName = targetColumn.Name }; } - var sourceTypeMapping = TypeMappingSource.GetMapping(source); - var targetTypeMapping = TypeMappingSource.GetMapping(target); - - var sourceColumnType = source.GetColumnType() - ?? sourceTypeMapping.StoreType; - var targetColumnType = target.GetColumnType() - ?? targetTypeMapping.StoreType; - var sourceMigrationsAnnotations = MigrationsAnnotations.For(source).ToList(); var targetMigrationsAnnotations = MigrationsAnnotations.For(target).ToList(); - var isSourceColumnNullable = source.IsColumnNullable(); - var isTargetColumnNullable = target.IsColumnNullable(); - var isNullableChanged = isSourceColumnNullable != isTargetColumnNullable; - var columnTypeChanged = sourceColumnType != targetColumnType; + var isNullableChanged = sourceColumn.IsNullable != targetColumn.IsNullable; + var columnTypeChanged = sourceColumn.Type != targetColumn.Type; if (isNullableChanged || columnTypeChanged @@ -956,25 +967,25 @@ protected virtual IEnumerable Diff( || source.GetComment() != target.GetComment() || HasDifferences(sourceMigrationsAnnotations, targetMigrationsAnnotations)) { - var isDestructiveChange = isNullableChanged && isSourceColumnNullable + var isDestructiveChange = isNullableChanged && sourceColumn.IsNullable // TODO: Detect type narrowing || columnTypeChanged; var alterColumnOperation = new AlterColumnOperation { - Schema = targetEntityType.GetSchema(), - Table = targetEntityType.GetTableName(), - Name = target.GetColumnName(), + Schema = table.Schema, + Table = table.Name, + Name = targetColumn.Name, IsDestructiveChange = isDestructiveChange }; Initialize( - alterColumnOperation, target, targetTypeMapping, - isTargetColumnNullable, targetMigrationsAnnotations, inline: true); + alterColumnOperation, target, targetMapping.TypeMapping, + targetColumn.IsNullable, targetMigrationsAnnotations, inline: true); Initialize( - alterColumnOperation.OldColumn, source, sourceTypeMapping, - isSourceColumnNullable, sourceMigrationsAnnotations, inline: true); + alterColumnOperation.OldColumn, source, sourceMapping.TypeMapping, + sourceColumn.IsNullable, sourceMigrationsAnnotations, inline: true); yield return alterColumnOperation; } @@ -991,17 +1002,19 @@ protected virtual IEnumerable Add( [NotNull] DiffContext diffContext, bool inline = false) { - var targetEntityType = target.DeclaringEntityType.GetRootType(); + var mapping = target.GetTableColumnMappings().Single(); + var column = mapping.Column; + var table = column.Table; var operation = new AddColumnOperation { - Schema = targetEntityType.GetSchema(), - Table = targetEntityType.GetTableName(), - Name = target.GetColumnName() + Schema = table.Schema, + Table = table.Name, + Name = column.Name }; Initialize( - operation, target, TypeMappingSource.GetMapping(target), target.IsColumnNullable(), + operation, target, mapping.TypeMapping, column.IsNullable, MigrationsAnnotations.For(target), inline); yield return operation; @@ -1015,13 +1028,15 @@ protected virtual IEnumerable Add( /// protected virtual IEnumerable Remove([NotNull] IProperty source, [NotNull] DiffContext diffContext) { - var sourceEntityType = source.DeclaringEntityType.GetRootType(); + var mapping = source.GetTableColumnMappings().Single(); + var column = mapping.Column; + var table = column.Table; var operation = new DropColumnOperation { - Schema = sourceEntityType.GetSchema(), - Table = sourceEntityType.GetTableName(), - Name = source.GetColumnName() + Schema = table.Schema, + Table = table.Name, + Name = column.Name }; operation.AddAnnotations(MigrationsAnnotations.ForRemove(source)); @@ -1199,8 +1214,8 @@ protected virtual IEnumerable Diff( (s, t, c) => s.GetConstraintName() == t.GetConstraintName() && s.Properties.Select(p => p.GetColumnName()).SequenceEqual( t.Properties.Select(p => c.FindSource(p)?.GetColumnName())) - && c.FindSourceTable(s.PrincipalEntityType) - == c.FindSource(c.FindTargetTable(t.PrincipalEntityType)) + && c.GetTable(s.PrincipalEntityType) + == c.FindSource(c.GetTable(t.PrincipalEntityType)) && s.PrincipalKey.Properties.Select(p => p.GetColumnName()).SequenceEqual( t.PrincipalKey.Properties.Select(p => c.FindSource(p)?.GetColumnName())) && ToReferentialAction(s.DeleteBehavior) == ToReferentialAction(t.DeleteBehavior) @@ -1406,7 +1421,7 @@ protected virtual IEnumerable Diff( Diff, Add, Remove, - (s, t, c) => c.FindSourceTable(s.EntityType) == c.FindSource(c.FindTargetTable(t.EntityType)) + (s, t, c) => c.GetTable(s.EntityType) == c.FindSource(c.GetTable(t.EntityType)) && string.Equals(s.Name, t.Name, StringComparison.OrdinalIgnoreCase) && string.Equals(s.Sql, t.Sql, StringComparison.OrdinalIgnoreCase)); @@ -1652,8 +1667,8 @@ protected virtual void TrackData( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected virtual void DiffData( - [NotNull] TableMapping source, - [NotNull] TableMapping target, + [NotNull] ITable source, + [NotNull] ITable target, [NotNull] DiffContext diffContext) { Check.NotNull(source, nameof(source)); @@ -1661,14 +1676,13 @@ protected virtual void DiffData( Check.NotNull(diffContext, nameof(diffContext)); var targetTableEntryMappingMap = SharedTableEntryMap>.CreateSharedTableEntryMapFactory( - target.EntityTypes, - _targetUpdateAdapter, - target.Name, - target.Schema) - ((t, s, c) => new List()); + target, + _targetUpdateAdapter) + ((_, __, ___) => new List()); - foreach (var targetEntityType in target.EntityTypes) + foreach (var targetMapping in target.EntityTypeMappings) { + var targetEntityType = targetMapping.EntityType; foreach (var targetSeed in targetEntityType.GetSeedData()) { var targetEntry = GetEntry(targetSeed, targetEntityType, _targetUpdateAdapter); @@ -1677,12 +1691,13 @@ protected virtual void DiffData( } } - var targetKeys = target.EntityTypes.SelectMany(EntityTypeExtensions.GetDeclaredKeys) - .Where(k => k.IsPrimaryKey()).ToList(); + var targetKeys = target.EntityTypeMappings + .SelectMany(m => m.EntityType.GetDeclaredKeys()).Where(k => k.IsPrimaryKey()).ToList(); var keyMapping = new Dictionary>>(); - foreach (var sourceEntityType in source.EntityTypes) + foreach (var sourceMapping in source.EntityTypeMappings) { + var sourceEntityType = sourceMapping.EntityType; foreach (var targetKey in targetKeys) { var keyPropertiesMap = new List<(IProperty, ValueConverter, ValueConverter)>(); @@ -1721,15 +1736,14 @@ protected virtual void DiffData( } var sourceTableEntryMappingMap = SharedTableEntryMap.CreateSharedTableEntryMapFactory( - source.EntityTypes, - _sourceUpdateAdapter, - source.Name, - source.Schema) - ((t, s, c) => new EntryMapping()); + source, + _sourceUpdateAdapter) + ((_, __, ___) => new EntryMapping()); _sharedTableEntryMaps.Add(sourceTableEntryMappingMap); - foreach (var sourceEntityType in source.EntityTypes) + foreach (var sourceMapping in source.EntityTypeMappings) { + var sourceEntityType = sourceMapping.EntityType; foreach (var sourceSeed in sourceEntityType.GetSeedData()) { var sourceEntry = GetEntry(sourceSeed, sourceEntityType, _sourceUpdateAdapter); @@ -2186,7 +2200,51 @@ private object GetDefaultValue(IProperty property) } private ValueConverter GetValueConverter(IProperty property) - => TypeMappingSource.GetMapping(property).Converter; + => property.GetValueConverter() ?? property.GetRelationalTypeMapping().Converter; + + private static IEntityType GetRootType(ITable table) + => table.EntityTypeMappings.Select(m => m.EntityType).FirstOrDefault( + t => t.BaseType == null && table.GetInternalForeignKeys(t) == 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 static IProperty[] GetMappedProperties([NotNull] ITable table, [NotNull] string[] names) + { + var properties = new IProperty[names.Length]; + for (var i = 0; i < names.Length; i++) + { + var name = names[i]; + var column = table.FindColumn(name); + if (column == null) + { + continue; + } + + properties[i] = column.PropertyMappings.First().Property; + } + + return properties; + } + + private static IEnumerable GetKeys(ITable table) + => table.EntityTypeMappings.SelectMany(m => m.EntityType.GetDeclaredKeys()) + .Distinct((x, y) => x.GetName() == y.GetName()); + + private static IEnumerable GetIndexes(ITable table) + => table.EntityTypeMappings.SelectMany(m => m.EntityType.GetDeclaredIndexes()) + .Distinct((x, y) => x.GetName() == y.GetName()); + + private static IEnumerable GetForeignKeys(ITable table) + => table.EntityTypeMappings.SelectMany(m => m.EntityType.GetDeclaredForeignKeys()) + .Distinct((x, y) => x.GetConstraintName() == y.GetConstraintName()) + .Where(fk => !fk.PrincipalKey.Properties + .Select(p => p.GetTableColumnMappings().Select(m => m.Column).SingleOrDefault(m => m.Table == table)) + .SequenceEqual(fk.Properties. + Select(p => p.GetTableColumnMappings().Select(m => m.Column).SingleOrDefault(m => m.Table == table)))); private static ReferentialAction ToReferentialAction(DeleteBehavior deleteBehavior) { @@ -2251,14 +2309,8 @@ private sealed class EntryMapping /// protected class DiffContext { - private readonly IReadOnlyList _sourceTables; - private readonly IReadOnlyList _targetTables; - - private readonly IDictionary _sourceEntitiesMap - = new Dictionary(); - - private readonly IDictionary _targetEntitiesMap - = new Dictionary(); + private readonly IEnumerable _sourceTables; + private readonly IEnumerable _targetTables; private readonly IDictionary _targetToSource = new Dictionary(); private readonly IDictionary _sourceToTarget = new Dictionary(); @@ -2269,8 +2321,8 @@ private readonly IDictionary _createTableOper private readonly IDictionary _dropTableOperations = new Dictionary(); - private readonly IDictionary _removedTables - = new Dictionary(); + private readonly IDictionary _removedTables + = new Dictionary(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2282,26 +2334,12 @@ public DiffContext([CanBeNull] IModel source, [CanBeNull] IModel target) { if (source != null) { - _sourceTables = TableMapping.GetTableMappings(source); - foreach (var table in _sourceTables) - { - foreach (var entityType in table.EntityTypes) - { - _sourceEntitiesMap.Add(entityType, table); - } - } + _sourceTables = source.GetTables(); } if (target != null) { - _targetTables = TableMapping.GetTableMappings(target); - foreach (var table in _targetTables) - { - foreach (var entityType in table.EntityTypes) - { - _targetEntitiesMap.Add(entityType, table); - } - } + _targetTables = target.GetTables(); } } @@ -2311,7 +2349,7 @@ public DiffContext([CanBeNull] IModel source, [CanBeNull] IModel target) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual IEnumerable GetSourceTables() => _sourceTables; + public virtual IEnumerable GetSourceTables() => _sourceTables; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2319,7 +2357,7 @@ public DiffContext([CanBeNull] IModel source, [CanBeNull] IModel target) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual IEnumerable GetTargetTables() => _targetTables; + public virtual IEnumerable GetTargetTables() => _targetTables; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2348,11 +2386,11 @@ public virtual void AddCreate([NotNull] IEntityType target, [NotNull] CreateTabl /// any release. You should only use it directly in your 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 AddDrop([NotNull] TableMapping source, [NotNull] DropTableOperation operation) + public virtual void AddDrop([NotNull] ITable source, [NotNull] DropTableOperation operation) { - foreach (var sourceEntityType in source.EntityTypes) + foreach (var sourceMapping in source.EntityTypeMappings) { - _dropTableOperations.Add(sourceEntityType, operation); + _dropTableOperations.Add(sourceMapping.EntityType, operation); } _removedTables.Add(operation, source); @@ -2364,21 +2402,8 @@ public virtual void AddDrop([NotNull] TableMapping source, [NotNull] DropTableOp /// any release. You should only use it directly in your 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 TableMapping FindSourceTable(IEntityType entityType) - => _sourceEntitiesMap.TryGetValue(entityType, out var table) - ? table - : 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 TableMapping FindTargetTable(IEntityType entityType) - => _targetEntitiesMap.TryGetValue(entityType, out var table) - ? table - : null; + public virtual ITable GetTable(IEntityType entityType) + => entityType.GetTableMappings().First().Table; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2408,10 +2433,14 @@ public virtual IProperty FindSource([NotNull] IProperty target) return source; } - var synonymousTargets = FindTargetTable(target.DeclaringEntityType).GetProperties() - .Where(p => p != target && p.GetColumnName() == target.GetColumnName()); + var synonymousTargets = target.GetTableColumnMappings().Single().Column.PropertyMappings.Select(m => m.Property); foreach (var synonymousTarget in synonymousTargets) { + if (synonymousTarget == target) + { + continue; + } + source = FindSource(synonymousTarget); if (source != null) { @@ -2466,7 +2495,7 @@ public virtual DropTableOperation FindDrop([NotNull] IEntityType source) /// any release. You should only use it directly in your 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 TableMapping FindTable([NotNull] DropTableOperation operation) + public virtual ITable FindTable([NotNull] DropTableOperation operation) => _removedTables.TryGetValue(operation, out var source) ? source : null; diff --git a/src/EFCore.Relational/Migrations/Operations/DeleteDataOperation.cs b/src/EFCore.Relational/Migrations/Operations/DeleteDataOperation.cs index 8535e842309..c3245837e7a 100644 --- a/src/EFCore.Relational/Migrations/Operations/DeleteDataOperation.cs +++ b/src/EFCore.Relational/Migrations/Operations/DeleteDataOperation.cs @@ -5,7 +5,7 @@ using System.Diagnostics; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Migrations.Internal; using Microsoft.EntityFrameworkCore.Update; using Microsoft.EntityFrameworkCore.Utilities; @@ -49,8 +49,9 @@ public virtual IEnumerable GenerateModificationCommands([Ca KeyColumns.Length == KeyValues.GetLength(1), $"The number of key values doesn't match the number of keys (${KeyColumns.Length})"); - var properties = model != null - ? TableMapping.GetTableMapping(model, Table, Schema)?.GetPropertyMap() + var table = model?.FindTable(Table, Schema); + var properties = table != null + ? MigrationsModelDiffer.GetMappedProperties(table, KeyColumns) : null; for (var i = 0; i < KeyValues.GetLength(0); i++) @@ -59,7 +60,7 @@ public virtual IEnumerable GenerateModificationCommands([Ca for (var j = 0; j < KeyColumns.Length; j++) { modifications[j] = new ColumnModification( - KeyColumns[j], originalValue: null, value: KeyValues[i, j], property: properties?.Find(KeyColumns[j]), + KeyColumns[j], originalValue: null, value: KeyValues[i, j], property: properties?[j], isRead: false, isWrite: true, isKey: true, isCondition: true, sensitiveLoggingEnabled: true); } diff --git a/src/EFCore.Relational/Migrations/Operations/InsertDataOperation.cs b/src/EFCore.Relational/Migrations/Operations/InsertDataOperation.cs index 09aeac5e88a..eeeafb4a32a 100644 --- a/src/EFCore.Relational/Migrations/Operations/InsertDataOperation.cs +++ b/src/EFCore.Relational/Migrations/Operations/InsertDataOperation.cs @@ -5,7 +5,7 @@ using System.Diagnostics; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Migrations.Internal; using Microsoft.EntityFrameworkCore.Update; using Microsoft.EntityFrameworkCore.Utilities; @@ -48,8 +48,9 @@ public virtual IEnumerable GenerateModificationCommands([Ca Columns.Length == Values.GetLength(1), $"The number of values doesn't match the number of keys (${Columns.Length})"); - var properties = model != null - ? TableMapping.GetTableMapping(model, Table, Schema)?.GetPropertyMap() + var table = model?.FindTable(Table, Schema); + var properties = table != null + ? MigrationsModelDiffer.GetMappedProperties(table, Columns) : null; for (var i = 0; i < Values.GetLength(0); i++) @@ -58,7 +59,7 @@ public virtual IEnumerable GenerateModificationCommands([Ca for (var j = 0; j < Columns.Length; j++) { modifications[j] = new ColumnModification( - Columns[j], originalValue: null, value: Values[i, j], property: properties?.Find(Columns[j]), + Columns[j], originalValue: null, value: Values[i, j], property: properties?[j], isRead: false, isWrite: true, isKey: true, isCondition: false, sensitiveLoggingEnabled: true); } diff --git a/src/EFCore.Relational/Migrations/Operations/UpdateDataOperation.cs b/src/EFCore.Relational/Migrations/Operations/UpdateDataOperation.cs index f21ae390048..8e4d5e18fe9 100644 --- a/src/EFCore.Relational/Migrations/Operations/UpdateDataOperation.cs +++ b/src/EFCore.Relational/Migrations/Operations/UpdateDataOperation.cs @@ -7,6 +7,7 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Migrations.Internal; using Microsoft.EntityFrameworkCore.Update; using Microsoft.EntityFrameworkCore.Utilities; @@ -67,8 +68,12 @@ public virtual IEnumerable GenerateModificationCommands([Ca KeyValues.GetLength(0) == Values.GetLength(0), $"The number of key values doesn't match the number of values (${KeyValues.GetLength(0)})"); - var properties = model != null - ? TableMapping.GetTableMapping(model, Table, Schema)?.GetPropertyMap() + var table = model?.FindTable(Table, Schema); + var keyProperties = table != null + ? MigrationsModelDiffer.GetMappedProperties(table, KeyColumns) + : null; + var properties = table != null + ? MigrationsModelDiffer.GetMappedProperties(table, Columns) : null; for (var i = 0; i < KeyValues.GetLength(0); i++) @@ -77,7 +82,7 @@ public virtual IEnumerable GenerateModificationCommands([Ca for (var j = 0; j < KeyColumns.Length; j++) { keys[j] = new ColumnModification( - KeyColumns[j], originalValue: null, value: KeyValues[i, j], property: properties?.Find(KeyColumns[j]), + KeyColumns[j], originalValue: null, value: KeyValues[i, j], property: keyProperties?[j], isRead: false, isWrite: false, isKey: true, isCondition: true, sensitiveLoggingEnabled: true); } @@ -85,7 +90,7 @@ public virtual IEnumerable GenerateModificationCommands([Ca for (var j = 0; j < Columns.Length; j++) { modifications[j] = new ColumnModification( - Columns[j], originalValue: null, value: Values[i, j], property: properties?.Find(Columns[j]), + Columns[j], originalValue: null, value: Values[i, j], property: properties?[j], isRead: false, isWrite: true, isKey: true, isCondition: false, sensitiveLoggingEnabled: true); } diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index d135541695d..1ec299ddc66 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -69,7 +69,7 @@ internal SelectExpression(IEntityType entityType) entityType, new TableExpression( entityType.GetTableName(), entityType.GetSchema(), - entityType.GetTableName().ToLower().Substring(0, 1))) + entityType.GetTableName().Substring(0, 1).ToLower())) { } @@ -78,7 +78,7 @@ internal SelectExpression(IEntityType entityType, string sql, Expression argumen entityType, new FromSqlExpression( sql, arguments, - entityType.GetTableName().ToLower().Substring(0, 1))) + (entityType.GetTableName() ?? entityType.GetViewName() ?? sql).Substring(0, 1).ToLower())) { } diff --git a/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs b/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs index 01c8c828224..4859c71764d 100644 --- a/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs +++ b/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs @@ -40,7 +40,7 @@ public class CommandBatchPreparer : ICommandBatchPreparer private readonly int _minBatchSize; private readonly bool _sensitiveLoggingEnabled; - private IReadOnlyDictionary<(string Schema, string Name), SharedTableEntryMapFactory> + private IReadOnlyDictionary<(string, string), SharedTableEntryMapFactory> _sharedTableEntryMapFactories; /// @@ -169,7 +169,7 @@ protected virtual IEnumerable CreateModificationCommands( .CreateSharedTableEntryMapFactories(updateAdapter.Model, updateAdapter); } - Dictionary<(string Schema, string Name), SharedTableEntryMap> sharedTablesCommandsMap = + Dictionary<(string Name, string Schema), SharedTableEntryMap> sharedTablesCommandsMap = null; foreach (var entry in entries) { @@ -182,7 +182,7 @@ protected virtual IEnumerable CreateModificationCommands( var entityType = entry.EntityType; var table = entityType.GetTableName(); var schema = entityType.GetSchema(); - var tableKey = (schema, table); + var tableKey = (table, schema); ModificationCommand command; var isMainEntry = true; @@ -191,19 +191,19 @@ protected virtual IEnumerable CreateModificationCommands( if (sharedTablesCommandsMap == null) { sharedTablesCommandsMap = - new Dictionary<(string Schema, string Name), SharedTableEntryMap>(); + new Dictionary<(string, string), SharedTableEntryMap>(); } if (!sharedTablesCommandsMap.TryGetValue(tableKey, out var sharedCommandsMap)) { sharedCommandsMap = commandIdentityMapFactory( - (t, s, c) => new ModificationCommand( - t, s, generateParameterName, _sensitiveLoggingEnabled, c)); - sharedTablesCommandsMap.Add((schema, table), sharedCommandsMap); + (n, s, c) => new ModificationCommand( + n, s, generateParameterName, _sensitiveLoggingEnabled, c)); + sharedTablesCommandsMap.Add(tableKey, sharedCommandsMap); } command = sharedCommandsMap.GetOrAddValue(entry); - isMainEntry = sharedCommandsMap.GetPrincipals(entry.EntityType.GetRootType()).Count == 0; + isMainEntry = sharedCommandsMap.IsMainEntityType(entry.EntityType.GetRootType()); } else { @@ -217,7 +217,7 @@ protected virtual IEnumerable CreateModificationCommands( if (sharedTablesCommandsMap != null) { - AddUnchangedSharingEntries(sharedTablesCommandsMap, entries); + AddUnchangedSharingEntries(sharedTablesCommandsMap.Values, entries); } return commands.Where( @@ -226,10 +226,10 @@ protected virtual IEnumerable CreateModificationCommands( } private void AddUnchangedSharingEntries( - Dictionary<(string Schema, string Name), SharedTableEntryMap> sharedTablesCommandsMap, + IEnumerable> sharedTablesCommands, IList entries) { - foreach (var sharedCommandsMap in sharedTablesCommandsMap.Values) + foreach (var sharedCommandsMap in sharedTablesCommands) { foreach (var command in sharedCommandsMap.Values) { @@ -247,7 +247,7 @@ private void AddUnchangedSharingEntries( entry.EntityState = EntityState.Modified; - var isMainEntry = sharedCommandsMap.GetPrincipals(entry.EntityType.GetRootType()).Count == 0; + var isMainEntry = sharedCommandsMap.IsMainEntityType(entry.EntityType.GetRootType()); command.AddEntry(entry, isMainEntry); entries.Add(entry); } diff --git a/src/EFCore.Relational/Update/Internal/SharedTableEntryMap.cs b/src/EFCore.Relational/Update/Internal/SharedTableEntryMap.cs index 1947d71090a..c54ff082adb 100644 --- a/src/EFCore.Relational/Update/Internal/SharedTableEntryMap.cs +++ b/src/EFCore.Relational/Update/Internal/SharedTableEntryMap.cs @@ -6,7 +6,6 @@ using System.Linq; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace Microsoft.EntityFrameworkCore.Update.Internal { @@ -18,11 +17,8 @@ namespace Microsoft.EntityFrameworkCore.Update.Internal /// public class SharedTableEntryMap { + private readonly ITable _table; private readonly IUpdateAdapter _updateAdapter; - private readonly IReadOnlyDictionary> _principals; - private readonly IReadOnlyDictionary> _dependents; - private readonly string _name; - private readonly string _schema; private readonly SharedTableEntryValueFactory _createElement; private readonly IComparer _comparer; @@ -36,20 +32,14 @@ private readonly Dictionary _entryValueMap /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public SharedTableEntryMap( + [NotNull] ITable table, [NotNull] IUpdateAdapter updateAdapter, - [NotNull] IReadOnlyDictionary> principals, - [NotNull] IReadOnlyDictionary> dependents, - [NotNull] string name, - [CanBeNull] string schema, [NotNull] SharedTableEntryValueFactory createElement) { + _table = table; _updateAdapter = updateAdapter; - _principals = principals; - _dependents = dependents; - _name = name; - _schema = schema; _createElement = createElement; - _comparer = new EntryComparer(principals); + _comparer = new EntryComparer(IsMainEntityType); } /// @@ -58,36 +48,22 @@ public SharedTableEntryMap( /// any release. You should only use it directly in your 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 Dictionary<(string Schema, string Name), SharedTableEntryMapFactory> + public static Dictionary<(string Name, string Schema), SharedTableEntryMapFactory> CreateSharedTableEntryMapFactories( [NotNull] IModel model, [NotNull] IUpdateAdapter updateAdapter) { - var tables = new Dictionary<(string Schema, string TableName), List>(); - foreach (var entityType in model.GetEntityTypes().Where(et => et.FindPrimaryKey() != null)) + var sharedTablesMap = new Dictionary<(string, string), SharedTableEntryMapFactory>(); + foreach (var table in model.GetTables()) { - var fullName = (entityType.GetSchema(), entityType.GetTableName()); - if (!tables.TryGetValue(fullName, out var mappedEntityTypes)) - { - mappedEntityTypes = new List(); - tables.Add(fullName, mappedEntityTypes); - } - - mappedEntityTypes.Add(entityType); - } - - var sharedTablesMap = new Dictionary<(string Schema, string Name), SharedTableEntryMapFactory>(); - foreach (var tableMapping in tables) - { - if (tableMapping.Value.Count <= 1) + if (!table.IsSplit) { continue; } - var factory = CreateSharedTableEntryMapFactory( - tableMapping.Value, updateAdapter, tableMapping.Key.TableName, tableMapping.Key.Schema); + var factory = CreateSharedTableEntryMapFactory(table, updateAdapter); - sharedTablesMap.Add(tableMapping.Key, factory); + sharedTablesMap.Add((table.Name, table.Schema), factory); } return sharedTablesMap; @@ -100,54 +76,13 @@ public SharedTableEntryMap( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public static SharedTableEntryMapFactory CreateSharedTableEntryMapFactory( - [NotNull] IReadOnlyList entityTypes, - [NotNull] IUpdateAdapter updateAdapter, - [NotNull] string tableName, - [NotNull] string schema) - { - var principals = new Dictionary>(entityTypes.Count); - var dependents = new Dictionary>(entityTypes.Count); - foreach (var entityType in entityTypes.Where(t => t.FindPrimaryKey() != null)) - { - var principalList = new List(); - foreach (var foreignKey in entityType.FindForeignKeys(entityType.FindPrimaryKey().Properties)) - { - if (foreignKey.PrincipalKey.IsPrimaryKey() - && entityTypes.Contains(foreignKey.PrincipalEntityType) - && !foreignKey.IsIntraHierarchical()) - { - principalList.Add(foreignKey.PrincipalEntityType); - } - } - - principals[entityType] = principalList; - - var dependentList = new List(); - foreach (var referencingForeignKey in entityType.FindPrimaryKey().GetReferencingForeignKeys()) - { - if (referencingForeignKey.PrincipalEntityType.IsAssignableFrom(entityType) - && entityTypes.Contains(referencingForeignKey.DeclaringEntityType) - && !referencingForeignKey.IsIntraHierarchical() - && (PropertyListComparer.Instance.Compare( - referencingForeignKey.DeclaringEntityType.FindPrimaryKey().Properties, - referencingForeignKey.Properties) - == 0)) - { - dependentList.Add(referencingForeignKey.DeclaringEntityType); - } - } - - dependents[entityType] = dependentList; - } - - return createElement => new SharedTableEntryMap( - updateAdapter, - principals, - dependents, - tableName, - schema, - createElement); - } + [NotNull] ITable table, + [NotNull] IUpdateAdapter updateAdapter) + => createElement + => new SharedTableEntryMap( + table, + updateAdapter, + createElement); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -171,7 +106,7 @@ public virtual TValue GetOrAddValue([NotNull] IUpdateEntry entry) return sharedCommand; } - sharedCommand = _createElement(_name, _schema, _comparer); + sharedCommand = _createElement(_table.Name, _table.Schema, _comparer); _entryValueMap.Add(mainEntry, sharedCommand); return sharedCommand; @@ -183,34 +118,23 @@ public virtual TValue GetOrAddValue([NotNull] IUpdateEntry entry) /// any release. You should only use it directly in your 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 GetPrincipals([NotNull] IEntityType entityType) => _principals[entityType]; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual IReadOnlyList GetDependents([NotNull] IEntityType entityType) => _dependents[entityType]; + public virtual bool IsMainEntityType([NotNull] IEntityType entityType) => _table.GetInternalForeignKeys(entityType) == null; private IUpdateEntry GetMainEntry(IUpdateEntry entry) { var entityType = entry.EntityType.GetRootType(); - if (_principals[entityType].Count == 0) + var foreignKeys = _table.GetInternalForeignKeys(entityType); + if (foreignKeys == null) { return entry; } - foreach (var foreignKey in entityType.FindForeignKeys(entityType.FindPrimaryKey().Properties)) + foreach (var foreignKey in foreignKeys) { - if (foreignKey.PrincipalKey.IsPrimaryKey() - && _principals.ContainsKey(foreignKey.PrincipalEntityType)) + var principalEntry = _updateAdapter.FindPrincipal(entry, foreignKey); + if (principalEntry != null) { - var principalEntry = _updateAdapter.FindPrincipal(entry, foreignKey); - if (principalEntry != null) - { - return GetMainEntry(principalEntry); - } + return GetMainEntry(principalEntry); } } @@ -234,34 +158,35 @@ public virtual IReadOnlyList GetAllEntries([NotNull] IUpdateEntry private void AddAllDependentsInclusive(IUpdateEntry entry, List entries) { entries.Add(entry); - foreach (var foreignKey in entry.EntityType.GetReferencingForeignKeys()) + var foreignKeys = _table.GetReferencingInternalForeignKeys(entry.EntityType); + if (foreignKeys == null) + { + return; + } + + foreach (var foreignKey in foreignKeys) { - if (foreignKey.PrincipalKey.IsPrimaryKey() - && foreignKey.IsUnique - && _dependents.ContainsKey(foreignKey.DeclaringEntityType)) + var dependentEntry = _updateAdapter.GetDependents(entry, foreignKey).SingleOrDefault(); + if (dependentEntry != null) { - var dependentEntry = _updateAdapter.GetDependents(entry, foreignKey).SingleOrDefault(); - if (dependentEntry != null) - { - AddAllDependentsInclusive(dependentEntry, entries); - } + AddAllDependentsInclusive(dependentEntry, entries); } } } private sealed class EntryComparer : IComparer { - private readonly IReadOnlyDictionary> _principals; + private readonly Func _isMain; - public EntryComparer(IReadOnlyDictionary> principals) + public EntryComparer(Func isMain) { - _principals = principals; + _isMain = isMain; } public int Compare(IUpdateEntry x, IUpdateEntry y) - => _principals[x.EntityType].Count == 0 + => _isMain(x.EntityType) ? -1 - : _principals[y.EntityType].Count == 0 + : _isMain(y.EntityType) ? 1 : StringComparer.Ordinal.Compare(x.EntityType.Name, y.EntityType.Name); } diff --git a/src/EFCore/Infrastructure/Annotatable.cs b/src/EFCore/Infrastructure/Annotatable.cs index ebafcb64e66..3b703668edf 100644 --- a/src/EFCore/Infrastructure/Annotatable.cs +++ b/src/EFCore/Infrastructure/Annotatable.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Diagnostics; @@ -197,18 +198,20 @@ public virtual object this[string name] protected virtual Annotation CreateAnnotation([NotNull] string name, [CanBeNull] object value) => new Annotation(name, value); - /// - /// Gets all annotations on the current object. - /// + /// + [DebuggerStepThrough] IEnumerable IAnnotatable.GetAnnotations() => GetAnnotations(); /// + [DebuggerStepThrough] IAnnotation IAnnotatable.FindAnnotation(string name) => FindAnnotation(name); /// + [DebuggerStepThrough] IAnnotation IMutableAnnotatable.AddAnnotation(string name, object value) => AddAnnotation(name, value); /// + [DebuggerStepThrough] IAnnotation IMutableAnnotatable.RemoveAnnotation(string name) => RemoveAnnotation(name); } } diff --git a/src/EFCore/Metadata/Internal/Model.cs b/src/EFCore/Metadata/Internal/Model.cs index 77641934354..653e428e7d0 100644 --- a/src/EFCore/Metadata/Internal/Model.cs +++ b/src/EFCore/Metadata/Internal/Model.cs @@ -890,13 +890,13 @@ protected override IConventionAnnotation OnAnnotationSet( /// public virtual IModel FinalizeModel() { - IModel validatedModel = ConventionDispatcher.OnModelFinalizing(Builder)?.Metadata; - if (validatedModel != null) + IModel finalizedModel = ConventionDispatcher.OnModelFinalizing(Builder)?.Metadata; + if (finalizedModel != null) { - validatedModel = ConventionDispatcher.OnModelFinalized(validatedModel); + finalizedModel = ConventionDispatcher.OnModelFinalized(finalizedModel); } - return (validatedModel as Model)?.MakeReadonly() ?? validatedModel; + return (finalizedModel as Model)?.MakeReadonly() ?? finalizedModel; } /// diff --git a/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs b/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs index 35a92e41fae..dcee13703fe 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs @@ -9,6 +9,9 @@ using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Conventions; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Migrations.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; @@ -54,8 +57,7 @@ private IMigrationsScaffolder CreateMigrationScaffolder() var sqlServerTypeMappingSource = new SqlServerTypeMappingSource( TestServiceFactory.Instance.Create(), TestServiceFactory.Instance.Create()); - var code = new CSharpHelper( - sqlServerTypeMappingSource); + var code = new CSharpHelper(sqlServerTypeMappingSource); var reporter = new TestOperationReporter(); var migrationAssembly = new MigrationsAssembly( @@ -66,11 +68,13 @@ var migrationAssembly var historyRepository = new MockHistoryRepository(); var services = RelationalTestHelpers.Instance.CreateContextServices(); + IModel model = new Model(); + model = new RelationalModelConvention().ProcessModelFinalized(model); return new MigrationsScaffolder( new MigrationsScaffolderDependencies( currentContext, - new Model(), + model, migrationAssembly, new MigrationsModelDiffer( new TestRelationalTypeMappingSource( @@ -98,7 +102,7 @@ var migrationAssembly historyRepository, reporter, new MockProvider(), - new SnapshotModelProcessor(reporter), + new SnapshotModelProcessor(reporter, services.GetRequiredService()), new Migrator( migrationAssembly, historyRepository, diff --git a/test/EFCore.Design.Tests/Migrations/Design/SnapshotModelProcessorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/SnapshotModelProcessorTest.cs index 1799c428ef9..51ff9968ab8 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/SnapshotModelProcessorTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/SnapshotModelProcessorTest.cs @@ -10,6 +10,7 @@ using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Conventions; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; @@ -51,7 +52,7 @@ public void Updates_provider_annotations_on_model() var reporter = new TestOperationReporter(); - new SnapshotModelProcessor(reporter).Process(model); + new SnapshotModelProcessor(reporter, null).Process(model); AssertAnnotations(model); AssertAnnotations(entityType); @@ -77,7 +78,7 @@ public void Warns_for_conflicting_annotations() var reporter = new TestOperationReporter(); - new SnapshotModelProcessor(reporter).Process(model); + new SnapshotModelProcessor(reporter, null).Process(model); Assert.Equal("warn: " + DesignStrings.MultipleAnnotationConflict("DefaultSchema"), reporter.Messages.Single()); Assert.Equal(2, model.GetAnnotations().Count()); @@ -98,7 +99,7 @@ public void Warns_for_conflicting_annotations_one_relational() var reporter = new TestOperationReporter(); - new SnapshotModelProcessor(reporter).Process(model); + new SnapshotModelProcessor(reporter, null).Process(model); Assert.Equal("warn: " + DesignStrings.MultipleAnnotationConflict("DefaultSchema"), reporter.Messages.Single()); Assert.Equal(2, model.GetAnnotations().Count()); @@ -119,7 +120,7 @@ public void Does_not_warn_for_duplicate_non_conflicting_annotations() var reporter = new TestOperationReporter(); - new SnapshotModelProcessor(reporter).Process(model); + new SnapshotModelProcessor(reporter, null).Process(model); Assert.Empty(reporter.Messages); @@ -138,7 +139,7 @@ public void Does_not_process_non_v1_models() var reporter = new TestOperationReporter(); - new SnapshotModelProcessor(reporter).Process(model); + new SnapshotModelProcessor(reporter, null).Process(model); Assert.Empty(reporter.Messages); @@ -164,7 +165,7 @@ public void Sets_owned_type_keys() }); var reporter = new TestOperationReporter(); - new SnapshotModelProcessor(reporter).Process(model); + new SnapshotModelProcessor(reporter, null).Process(model); Assert.Empty(reporter.Messages); Assert.Equal( @@ -180,13 +181,13 @@ public void Sets_owned_type_keys() [InlineData(typeof(OwnershipModelSnapshot3_0))] public void Can_diff_against_older_ownership_model(Type snapshotType) { - using var db = new Ownership.OwnershipContext(); - var differ = db.GetService(); + using var context = new Ownership.OwnershipContext(); + var differ = context.GetService(); var snapshot = (ModelSnapshot)Activator.CreateInstance(snapshotType); var reporter = new TestOperationReporter(); - var processor = new SnapshotModelProcessor(reporter); + var processor = new SnapshotModelProcessor(reporter, context.GetService()); - var differences = differ.GetDifferences(processor.Process(snapshot.Model), db.Model); + var differences = differ.GetDifferences(processor.Process(snapshot.Model), context.Model); Assert.Empty(differences); } diff --git a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs index ae2354a40a3..48aaf6c33ff 100644 --- a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs +++ b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs @@ -12,6 +12,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Conventions; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Migrations.Design; using Microsoft.EntityFrameworkCore.Migrations.Internal; @@ -242,7 +243,7 @@ public virtual void Model_annotations_are_stored_in_snapshot() .HasAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.IdentityColumn);"), o => { - Assert.Equal(6, o.GetAnnotations().Count()); + Assert.Equal(7, o.GetAnnotations().Count()); Assert.Equal("AnnotationValue", o["AnnotationName"]); }); } @@ -265,7 +266,7 @@ public virtual void Model_default_schema_annotation_is_stored_in_snapshot_as_flu .HasAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.IdentityColumn);"), o => { - Assert.Equal(4, o.GetAnnotations().Count()); + Assert.Equal(5, o.GetAnnotations().Count()); Assert.Equal("AnnotationValue", o["AnnotationName"]); Assert.Equal("DefaultSchema", o[RelationalAnnotationNames.DefaultSchema]); }); @@ -351,7 +352,7 @@ public virtual void Sequence_is_stored_in_snapshot_as_annotations() .HasAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.IdentityColumn);"), o => { - Assert.Equal(3, o.GetAnnotations().Count()); + Assert.Equal(4, o.GetAnnotations().Count()); }); } @@ -415,7 +416,7 @@ public virtual void CheckConstraint_is_stored_in_snapshot_as_fluent_api() });"), o => { - Assert.Equal(2, o.GetAnnotations().Count()); + Assert.Equal(3, o.GetAnnotations().Count()); }); } @@ -464,7 +465,7 @@ public virtual void CheckConstraint_is_only_stored_in_snapshot_once_for_TPH() });"), o => { - Assert.Equal(2, o.GetAnnotations().Count()); + Assert.Equal(3, o.GetAnnotations().Count()); }); } @@ -499,7 +500,7 @@ public virtual void EntityType_annotations_are_stored_in_snapshot() });"), o => { - Assert.Equal(2, o.GetEntityTypes().First().GetAnnotations().Count()); + Assert.Equal(3, o.GetEntityTypes().First().GetAnnotations().Count()); Assert.Equal("AnnotationValue", o.GetEntityTypes().First()["AnnotationName"]); }); } @@ -2086,7 +2087,7 @@ public virtual void Property_multiple_annotations_are_stored_in_snapshot() o => { var property = o.GetEntityTypes().First().FindProperty("AlternateId"); - Assert.Equal(3, property.GetAnnotations().Count()); + Assert.Equal(5, property.GetAnnotations().Count()); Assert.Equal("AnnotationValue", property["AnnotationName"]); Assert.Equal("CName", property["Relational:ColumnName"]); Assert.Equal("int", property["Relational:ColumnType"]); @@ -3749,7 +3750,11 @@ protected void Test(IModel model, string expectedCode, Action as Activator.CreateInstance(factoryType), new object[] { builder }); - var modelFromSnapshot = new SnapshotModelProcessor(new TestOperationReporter()).Process(builder.Model); + var services = RelationalTestHelpers.Instance.CreateContextServices(); + + var processor = new SnapshotModelProcessor(new TestOperationReporter(), + services.GetRequiredService().With(sqlServerTypeMappingSource)); + var modelFromSnapshot = processor.Process(builder.Model); assert(modelFromSnapshot, model); } diff --git a/test/EFCore.Relational.Specification.Tests/MigrationsInfrastructureTestBase.cs b/test/EFCore.Relational.Specification.Tests/MigrationsInfrastructureTestBase.cs index f78c0c19a0a..53c8cd41715 100644 --- a/test/EFCore.Relational.Specification.Tests/MigrationsInfrastructureTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/MigrationsInfrastructureTestBase.cs @@ -1,11 +1,14 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Conventions; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.TestUtilities; @@ -284,23 +287,15 @@ public virtual void Can_get_active_provider() protected virtual void DiffSnapshot(ModelSnapshot snapshot, DbContext context) { - var sourceModel = ((IMutableModel)snapshot.Model).FinalizeModel(); - var targetModel = context.Model; + var dependencies = context.GetService(); + var typeMappingConvention = new TypeMappingConvention(dependencies); + typeMappingConvention.ProcessModelFinalizing(((IConventionModel)snapshot.Model).Builder, null); - var typeMapper = context.GetService(); - - foreach (var property in sourceModel.GetEntityTypes().SelectMany(e => e.GetDeclaredProperties())) - { - Assert.NotNull(typeMapper.FindMapping(property)); - } - - foreach (var property in targetModel.GetEntityTypes().SelectMany(e => e.GetDeclaredProperties())) - { - Assert.NotNull(typeMapper.FindMapping(property)); - } + var relationalModelConvention = new RelationalModelConvention(); + var sourceModel = relationalModelConvention.ProcessModelFinalized(snapshot.Model); var modelDiffer = context.GetService(); - var operations = modelDiffer.GetDifferences(sourceModel, targetModel); + var operations = modelDiffer.GetDifferences(((IMutableModel)sourceModel).FinalizeModel(), context.Model); Assert.Equal(0, operations.Count); } diff --git a/test/EFCore.Relational.Specification.Tests/MigrationsTestBase.cs b/test/EFCore.Relational.Specification.Tests/MigrationsTestBase.cs index 02cd262c9d8..50009ee91f6 100644 --- a/test/EFCore.Relational.Specification.Tests/MigrationsTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/MigrationsTestBase.cs @@ -1447,6 +1447,7 @@ protected virtual ModelBuilder CreateConventionlessModelBuilder(bool sensitiveDa var dependencies = Fixture.TestHelpers.CreateContextServices().GetRequiredService(); conventionSet.ModelFinalizingConventions.Add(new TypeMappingConvention(dependencies)); + conventionSet.ModelFinalizedConventions.Add(new RelationalModelConvention()); return new ModelBuilder(conventionSet); } diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs new file mode 100644 index 00000000000..021d70c40a3 --- /dev/null +++ b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs @@ -0,0 +1,145 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; + +// ReSharper disable InconsistentNaming + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + public class RelationalModelTest + { + [ConditionalFact] + public void Can_use_relational_model() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder.Entity(); + modelBuilder.Entity(ob => + { + ob.Property(od => od.OrderDate).HasColumnName("OrderDate"); + ob.OwnsOne(o => o.Details, odb => + { + odb.Property(od => od.OrderDate).HasColumnName("OrderDate"); + odb.OwnsOne(od => od.BillingAddress); + odb.OwnsOne(od => od.ShippingAddress); + }); + }); + + var model = modelBuilder.FinalizeModel(); + + Assert.Equal(6, model.GetEntityTypes().Count()); + + var orderType = model.FindEntityType(typeof(Order)); + var orderMapping = orderType.GetTableMappings().Single(); + Assert.True(orderMapping.IncludesDerivedTypes); + Assert.Equal( + new[] { nameof(Order.CustomerId), nameof(Order.OrderDate), nameof(Order.OrderId) }, + orderMapping.ColumnMappings.Select(m => m.Property.Name)); + + var ordersTable = orderMapping.Table; + Assert.Equal( + new[] { "OrderDetails.BillingAddress#Address", "OrderDetails.ShippingAddress#Address", nameof(Order), nameof(OrderDetails) }, + ordersTable.EntityTypeMappings.Select(m => m.EntityType.DisplayName())); + Assert.Equal(new[] { + nameof(Order.CustomerId), + "Details_BillingAddress_City", + "Details_BillingAddress_Street", + "Details_ShippingAddress_City", + "Details_ShippingAddress_Street", + nameof(Order.OrderDate), + nameof(Order.OrderId) + }, + ordersTable.Columns.Select(m => m.Name)); + Assert.Equal("Order", ordersTable.Name); + Assert.Null(ordersTable.Schema); + Assert.True(ordersTable.IsMigratable); + Assert.True(ordersTable.IsSplit); + + var orderDate = orderType.FindProperty(nameof(Order.OrderDate)); + Assert.False(orderDate.IsColumnNullable()); + + var orderDateMapping = orderDate.GetTableColumnMappings().Single(); + Assert.NotNull(orderDateMapping.TypeMapping); + Assert.Equal("default_datetime_mapping", orderDateMapping.TypeMapping.StoreType); + Assert.Same(orderMapping, orderDateMapping.TableMapping); + + var orderDetailsOwnership = orderType.FindNavigation(nameof(Order.Details)).ForeignKey; + var orderDetailsType = orderDetailsOwnership.DeclaringEntityType; + Assert.Single(orderDetailsType.GetTableMappings()); + Assert.Equal(ordersTable.GetReferencingInternalForeignKeys(orderType), ordersTable.GetInternalForeignKeys(orderDetailsType)); + + var orderDetailsDate = orderDetailsType.FindProperty(nameof(OrderDetails.OrderDate)); + Assert.True(orderDetailsDate.IsColumnNullable()); + + var orderDateColumn = orderDateMapping.Column; + Assert.Same(orderDateColumn, ordersTable.FindColumn("OrderDate")); + Assert.Equal(new[] { orderDate, orderDetailsDate }, orderDateColumn.PropertyMappings.Select(m => m.Property)); + Assert.Equal("OrderDate", orderDateColumn.Name); + Assert.Equal("default_datetime_mapping", orderDateColumn.Type); + Assert.False(orderDateColumn.IsNullable); + Assert.Same(ordersTable, orderDateColumn.Table); + + var customerType = model.FindEntityType(typeof(Customer)); + Assert.Single(customerType.GetTableMappings()); + + var specialCustomerType = model.FindEntityType(typeof(SpecialCustomer)); + Assert.Single(specialCustomerType.GetTableMappings()); + } + + protected virtual ModelBuilder CreateConventionModelBuilder() => RelationalTestHelpers.Instance.CreateConventionBuilder(); + + private enum MyEnum : ulong + { + Sun, + Mon, + Tue + } + + private class Customer + { + public int Id { get; set; } + public string Name { get; set; } + public short SomeShort { get; set; } + public MyEnum EnumValue { get; set; } + + public IEnumerable Orders { get; set; } + } + + private class SpecialCustomer : Customer + { + public string Speciality { get; set; } + } + + private class Order + { + public int OrderId { get; set; } + public DateTime OrderDate { get; set; } + + public int CustomerId { get; set; } + public Customer Customer { get; set; } + + public OrderDetails Details { get; set; } + } + + private class OrderDetails + { + public DateTime OrderDate { get; set; } + + public int OrderId { get; set; } + public Order Order { get; set; } + public Address BillingAddress { get; set; } + public Address ShippingAddress { get; set; } + } + + private class Address + { + public string Street { get; set; } + public string City { get; set; } + } + } +} diff --git a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTestBase.cs b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTestBase.cs index 1218746c3c1..b2b3be0e577 100644 --- a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTestBase.cs +++ b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTestBase.cs @@ -6,12 +6,14 @@ using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Conventions; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations.Operations; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.TestUtilities; using Microsoft.EntityFrameworkCore.Update; using Microsoft.EntityFrameworkCore.Update.Internal; using Microsoft.EntityFrameworkCore.Utilities; +using Microsoft.Extensions.DependencyInjection; using Xunit; namespace Microsoft.EntityFrameworkCore.Migrations.Internal @@ -138,9 +140,18 @@ protected static T[][] ToJaggedArray(T[,] twoDimensionalArray, bool firstDime protected virtual ModelBuilder CreateModelBuilder(bool skipConventions) => skipConventions - ? new ModelBuilder(new ConventionSet()) + ? new ModelBuilder(CreateEmptyConventionSet()) : TestHelpers.CreateConventionBuilder(skipValidation: true); + private ConventionSet CreateEmptyConventionSet() + { + var conventions = new ConventionSet(); + var conventionSetDependencies = TestHelpers.CreateContextServices().GetRequiredService(); + conventions.ModelFinalizingConventions.Add(new TypeMappingConvention(conventionSetDependencies)); + conventions.ModelFinalizedConventions.Add(new RelationalModelConvention()); + return conventions; + } + protected virtual MigrationsModelDiffer CreateModelDiffer(DbContextOptions options) { var ctx = TestHelpers.CreateContext(options); diff --git a/test/EFCore.Specification.Tests/TestUtilities/TestTypeMappingSource.cs b/test/EFCore.Specification.Tests/TestUtilities/TestTypeMappingSource.cs new file mode 100644 index 00000000000..a078f1368e1 --- /dev/null +++ b/test/EFCore.Specification.Tests/TestUtilities/TestTypeMappingSource.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.TestUtilities +{ + public class TestTypeMappingSource : TypeMappingSource + { + public TestTypeMappingSource([NotNull] TypeMappingSourceDependencies dependencies) + : base(dependencies) + { + } + + protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo) + { + return null; + } + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/InheritanceSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/InheritanceSqlServerTest.cs index 84b1ad1de0b..12ae1bc48f6 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/InheritanceSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/InheritanceSqlServerTest.cs @@ -215,12 +215,12 @@ public override void Can_query_all_animal_views() base.Can_query_all_animal_views(); AssertSql( - @"SELECT [a].[CountryId], [a].[Discriminator], [a].[Name], [a].[EagleId], [a].[IsFlightless], [a].[Group], [a].[FoundOn] + @"SELECT [s].[CountryId], [s].[Discriminator], [s].[Name], [s].[EagleId], [s].[IsFlightless], [s].[Group], [s].[FoundOn] FROM ( SELECT * FROM Animal -) AS [a] -WHERE [a].[Discriminator] IN (N'Eagle', N'Kiwi') -ORDER BY [a].[CountryId]"); +) AS [s] +WHERE [s].[Discriminator] IN (N'Eagle', N'Kiwi') +ORDER BY [s].[CountryId]"); } public override void Can_query_all_plants()