diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index 6c780f76247..bb8217ab7d8 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -62,7 +62,8 @@ public virtual void Generate(string builderName, IModel model, IndentedStringBui CoreAnnotationNames.ChangeTrackingStrategy, CoreAnnotationNames.OwnedTypes, RelationalAnnotationNames.CheckConstraints, - RelationalAnnotationNames.Tables); + RelationalAnnotationNames.Tables, + RelationalAnnotationNames.Views); if (annotations.Count > 0) { @@ -497,6 +498,7 @@ protected virtual void GeneratePropertyAnnotations([NotNull] IProperty property, annotations, RelationalAnnotationNames.ColumnType, RelationalAnnotationNames.TableColumnMappings, + RelationalAnnotationNames.ViewColumnMappings, CoreAnnotationNames.ValueGeneratorFactory, CoreAnnotationNames.PropertyAccessMode, CoreAnnotationNames.ChangeTrackingStrategy, @@ -787,7 +789,8 @@ protected virtual void GenerateEntityTypeAnnotations( CoreAnnotationNames.DefiningQuery, CoreAnnotationNames.QueryFilter, RelationalAnnotationNames.CheckConstraints, - RelationalAnnotationNames.TableMappings); + RelationalAnnotationNames.TableMappings, + RelationalAnnotationNames.ViewMappings); if (annotations.Count > 0) { diff --git a/src/EFCore.Design/Migrations/Design/MigrationsCodeGenerator.cs b/src/EFCore.Design/Migrations/Design/MigrationsCodeGenerator.cs index 1fad485aec6..7ba24ef6c2d 100644 --- a/src/EFCore.Design/Migrations/Design/MigrationsCodeGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/MigrationsCodeGenerator.cs @@ -244,7 +244,10 @@ private IEnumerable GetAnnotationNamespaces(IEnumerable it RelationalAnnotationNames.CheckConstraints, RelationalAnnotationNames.Tables, RelationalAnnotationNames.TableMappings, - RelationalAnnotationNames.TableColumnMappings + RelationalAnnotationNames.TableColumnMappings, + RelationalAnnotationNames.Views, + RelationalAnnotationNames.ViewMappings, + RelationalAnnotationNames.ViewColumnMappings }; var ignoredAnnotationTypes = new List diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs index f1c293cf800..f071ed31970 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs @@ -264,6 +264,7 @@ protected virtual void GenerateOnModelCreating( RemoveAnnotation(ref annotations, RelationalAnnotationNames.MaxIdentifierLength); RemoveAnnotation(ref annotations, RelationalAnnotationNames.CheckConstraints); RemoveAnnotation(ref annotations, RelationalAnnotationNames.Tables); + RemoveAnnotation(ref annotations, RelationalAnnotationNames.Views); RemoveAnnotation(ref annotations, ScaffoldingAnnotationNames.DatabaseName); RemoveAnnotation(ref annotations, ScaffoldingAnnotationNames.EntityTypeErrors); @@ -366,6 +367,7 @@ private void GenerateEntityType(IEntityType entityType, bool useDataAnnotations) RemoveAnnotation(ref annotations, RelationalAnnotationNames.Comment); RemoveAnnotation(ref annotations, RelationalAnnotationNames.Schema); RemoveAnnotation(ref annotations, RelationalAnnotationNames.TableMappings); + RemoveAnnotation(ref annotations, RelationalAnnotationNames.ViewMappings); RemoveAnnotation(ref annotations, ScaffoldingAnnotationNames.DbSetName); RemoveAnnotation(ref annotations, RelationalAnnotationNames.ViewDefinition); @@ -616,6 +618,7 @@ private void GenerateProperty(IProperty property, bool useDataAnnotations) RemoveAnnotation(ref annotations, RelationalAnnotationNames.ComputedColumnSql); RemoveAnnotation(ref annotations, RelationalAnnotationNames.IsFixedLength); RemoveAnnotation(ref annotations, RelationalAnnotationNames.TableColumnMappings); + RemoveAnnotation(ref annotations, RelationalAnnotationNames.ViewColumnMappings); RemoveAnnotation(ref annotations, ScaffoldingAnnotationNames.ColumnOrdinal); if (!useDataAnnotations) diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs index 4f642c24cdc..13582bc4334 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; +using System.Linq; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -146,13 +147,29 @@ public static void SetSchema( ?.GetConfigurationSource(); /// - /// Returns the name of the table to which the entity type is mapped. + /// Returns the tables 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 views or tables 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 GetViewOrTableMappings([NotNull] this IEntityType entityType) => + (IEnumerable)GetViewMappings(entityType) ?? GetTableMappings(entityType); + + /// + /// Returns the views 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 GetViewMappings([NotNull] this IEntityType entityType) => + (IEnumerable)entityType[RelationalAnnotationNames.ViewMappings]; + /// /// Returns the name of the view to which the entity type is mapped. /// diff --git a/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs b/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs index ac185a02bab..4666e7bf083 100644 --- a/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs @@ -61,7 +61,8 @@ public static void SetDefaultSchema( /// 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; + ((IDictionary<(string, string), Table>)model[RelationalAnnotationNames.Tables])?.Values + ?? Enumerable.Empty(); /// /// Gets the table with a given name. Returns null if no table with the given name is defined. @@ -70,10 +71,39 @@ public static IEnumerable GetTables([NotNull] this IModel model) => /// 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) + public static ITable FindTable([NotNull] this IModel model, [NotNull] string name, [CanBeNull] string schema) + { + Table table = null; + return ((IDictionary<(string, string), Table>)model[RelationalAnnotationNames.Tables]) +?.TryGetValue((name, schema), out table) == true ? table : null; + } + + /// + /// Returns all the views mapped in the model. + /// + /// The model to get the tables for. + /// All the tables mapped in the model. + public static IEnumerable GetViews([NotNull] this IModel model) => + ((IDictionary<(string, string), View>)model[RelationalAnnotationNames.Views])?.Values + ?? Enumerable.Empty(); + + /// + /// Gets the view with a given name. Returns null if no view with the given name is defined. + /// + /// The model to get the view for. + /// The name of the view. + /// The schema of the view. + /// The view with a given name or null if no view with the given name is defined. + public static IView FindView([NotNull] this IModel model, [NotNull] string name, [CanBeNull] string schema) + { + View view = null; + return ((IDictionary<(string, string), View>)model[RelationalAnnotationNames.Views]) + ?.TryGetValue((name, schema), out view) == true + ? view + : 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 e7fdbed9324..3e18f2fc08b 100644 --- a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs @@ -175,13 +175,29 @@ public static void SetColumnType( => property.FindAnnotation(RelationalAnnotationNames.ColumnType)?.GetConfigurationSource(); /// - /// Returns the columns to which the property is mapped. + /// Returns the table columns to which the property is mapped. /// /// The property. - /// The name of the table to which the entity type is mapped. + /// The table columns to which the property is mapped. public static IEnumerable GetTableColumnMappings([NotNull] this IProperty property) => (IEnumerable)property[RelationalAnnotationNames.TableColumnMappings]; + /// + /// Returns the view or table columns to which the property is mapped. + /// + /// The property. + /// The view or table columns to which the property is mapped. + public static IEnumerable GetViewOrTableColumnMappings([NotNull] this IProperty property) => + (IEnumerable)GetViewColumnMappings(property) ?? GetTableColumnMappings(property); + + /// + /// Returns the view columns to which the property is mapped. + /// + /// The property. + /// The view columns to which the property is mapped. + public static IEnumerable GetViewColumnMappings([NotNull] this IProperty property) => + (IEnumerable)property[RelationalAnnotationNames.ViewColumnMappings]; + /// /// 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/Metadata/ITable.cs b/src/EFCore.Relational/Metadata/ITable.cs index da5d207e5d3..a9f284dd187 100644 --- a/src/EFCore.Relational/Metadata/ITable.cs +++ b/src/EFCore.Relational/Metadata/ITable.cs @@ -30,36 +30,21 @@ public interface ITable : ITableBase bool IsMigratable { get; } /// - /// Returns a value indicating whether multiple entity types are sharing the rows in the table. + /// The check constraints for this 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() + IEnumerable CheckConstraints => EntityTypeMappings.SelectMany(m => CheckConstraint.GetCheckConstraints(m.EntityType)) .Distinct((x, y) => x.Name == y.Name); /// - /// Returns the comment for this table. + /// The comment for this table. /// - public virtual string GetComment() + public virtual string Comment => EntityTypeMappings.Select(e => e.EntityType.GetComment()).FirstOrDefault(c => c != null); + + /// + /// Returns the column with a given name. Returns null if no column with the given name is defined. + /// + new IColumn FindColumn([NotNull] string name); } } diff --git a/src/EFCore.Relational/Metadata/ITableBase.cs b/src/EFCore.Relational/Metadata/ITableBase.cs index 11d1b6ec83b..5b768a53304 100644 --- a/src/EFCore.Relational/Metadata/ITableBase.cs +++ b/src/EFCore.Relational/Metadata/ITableBase.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Infrastructure; namespace Microsoft.EntityFrameworkCore.Metadata @@ -21,6 +22,11 @@ public interface ITableBase : IAnnotatable /// string Schema { get; } + /// + /// Returns a value indicating whether multiple entity types are sharing the rows in the table. + /// + bool IsSplit { get; } + /// /// The entity type mappings. /// @@ -30,5 +36,20 @@ public interface ITableBase : IAnnotatable /// The columns defined for this table. /// IEnumerable Columns { get; } + + /// + /// Returns the column with a given name. Returns null if no column with the given name is defined. + /// + IColumnBase 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); } } diff --git a/src/EFCore.Relational/Metadata/IView.cs b/src/EFCore.Relational/Metadata/IView.cs new file mode 100644 index 00000000000..7bdb7f2afde --- /dev/null +++ b/src/EFCore.Relational/Metadata/IView.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 JetBrains.Annotations; + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + /// + /// Represents a view in the database. + /// + public interface IView : ITableBase + { + /// + /// The entity type mappings. + /// + new IEnumerable EntityTypeMappings { get; } + + /// + /// The columns defined for this view. + /// + new IEnumerable Columns { get; } + + /// + /// Returns the column with a given name. Returns null if no column with the given name is defined. + /// + new IViewColumn FindColumn([NotNull] string name); + + /// + /// The view definition or null if this view is not managed by migrations. + /// + public string ViewDefinition { get; } + } +} diff --git a/src/EFCore.Relational/Metadata/IViewColumn.cs b/src/EFCore.Relational/Metadata/IViewColumn.cs new file mode 100644 index 00000000000..2db3a3dc32c --- /dev/null +++ b/src/EFCore.Relational/Metadata/IViewColumn.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 view. + /// + public interface IViewColumn : IColumnBase + { + /// + /// The containing view. + /// + IView View { get; } + + /// + /// The property mappings. + /// + new IEnumerable PropertyMappings { get; } + } +} diff --git a/src/EFCore.Relational/Metadata/IViewColumnMapping.cs b/src/EFCore.Relational/Metadata/IViewColumnMapping.cs new file mode 100644 index 00000000000..68c9e3634d4 --- /dev/null +++ b/src/EFCore.Relational/Metadata/IViewColumnMapping.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 IViewColumnMapping : IColumnMappingBase + { + /// + /// The target column. + /// + new IViewColumn Column { get; } + + /// + /// The containing view mapping. + /// + IViewMapping ViewMapping { get; } + } +} diff --git a/src/EFCore.Relational/Metadata/IViewMapping.cs b/src/EFCore.Relational/Metadata/IViewMapping.cs new file mode 100644 index 00000000000..c8d72100cb9 --- /dev/null +++ b/src/EFCore.Relational/Metadata/IViewMapping.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 view. + /// + public interface IViewMapping : ITableMappingBase + { + /// + /// The target view. + /// + IView View { get; } + + /// + /// The properties mapped to columns on the target view. + /// + new IEnumerable ColumnMappings { get; } + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/Column.cs b/src/EFCore.Relational/Metadata/Internal/Column.cs index fef8726a01a..4c2b2fa9b64 100644 --- a/src/EFCore.Relational/Metadata/Internal/Column.cs +++ b/src/EFCore.Relational/Metadata/Internal/Column.cs @@ -22,7 +22,7 @@ public class Column : Annotatable, IColumn /// any release. You should only use it directly in your 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) + public Column([NotNull] string name, [NotNull] string type, [NotNull] Table table) { Name = name; Type = type; diff --git a/src/EFCore.Relational/Metadata/Internal/ColumnMapping.cs b/src/EFCore.Relational/Metadata/Internal/ColumnMapping.cs index 22103cd8b3e..f33c78a682a 100644 --- a/src/EFCore.Relational/Metadata/Internal/ColumnMapping.cs +++ b/src/EFCore.Relational/Metadata/Internal/ColumnMapping.cs @@ -24,9 +24,9 @@ public class ColumnMapping : Annotatable, IColumnMapping /// public ColumnMapping( [NotNull] IProperty property, - [NotNull] IColumn column, + [NotNull] Column column, [NotNull] RelationalTypeMapping typeMapping, - [NotNull] ITableMapping tableMapping) + [NotNull] TableMapping tableMapping) { Property = property; Column = column; diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs index 90f2a5a7a3b..9978a4ecd70 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs @@ -24,136 +24,207 @@ public class RelationalModel public static IModel AddRelationalModel([NotNull] IConventionModel model) { var tables = new SortedDictionary<(string, string), Table>(); + var views = new SortedDictionary<(string, string), View>(); foreach (var entityType in model.GetEntityTypes()) { var tableName = entityType.GetTableName(); - if (tableName == null - || entityType.GetViewName() != null) + var viewName = entityType.GetViewName(); + if (tableName != null + && viewName == null) { - continue; - } + var schema = entityType.GetSchema(); + if (!tables.TryGetValue((tableName, schema), out var table)) + { + table = new Table(tableName, schema); + tables.Add((tableName, schema), table); + } - var schema = entityType.GetSchema(); - if (!tables.TryGetValue((tableName, schema), out var table)) - { - table = new Table(tableName, schema); - tables.Add((tableName, schema), table); - } + table.IsMigratable = table.IsMigratable + || entityType.FindAnnotation(RelationalAnnotationNames.ViewDefinition) == null; - 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 = (Column)table.FindColumn(columnName); + 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 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) + 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) { - column = new Column(columnName, property.GetColumnType() ?? typeMapping.StoreType, table); - column.IsNullable = property.IsColumnNullable(); - table.Columns.Add(columnName, column); + tableMappings = new SortedSet(TableMappingComparer.Instance); + entityType.SetAnnotation(RelationalAnnotationNames.TableMappings, tableMappings); } - else if (!property.IsColumnNullable()) + + tableMappings.Add(tableMapping); + table.EntityTypeMappings.Add(tableMapping); + } + + if (viewName != null) + { + var schema = entityType.GetSchema(); + if (!views.TryGetValue((viewName, schema), out var view)) { - column.IsNullable = false; + view = new View(viewName, schema); + views.Add((viewName, schema), view); } - var columnMapping = new ColumnMapping(property, column, typeMapping, tableMapping); - tableMapping.ColumnMappings.Add(columnMapping); - column.PropertyMappings.Add(columnMapping); + var viewMapping = new ViewMapping(entityType, view, includesDerivedTypes: true); + foreach (var property in entityType.GetDeclaredProperties()) + { + var typeMapping = property.GetRelationalTypeMapping(); + var columnName = property.GetColumnName(); + var column = (ViewColumn)view.FindColumn(columnName); + if (column == null) + { + column = new ViewColumn(columnName, property.GetColumnType() ?? typeMapping.StoreType, view); + column.IsNullable = property.IsColumnNullable(); + view.Columns.Add(columnName, column); + } + else if (!property.IsColumnNullable()) + { + column.IsNullable = false; + } + + var columnMapping = new ViewColumnMapping(property, column, typeMapping, viewMapping); + viewMapping.ColumnMappings.Add(columnMapping); + column.PropertyMappings.Add(columnMapping); + + var columnMappings = property[RelationalAnnotationNames.ViewColumnMappings] as SortedSet; + if (columnMappings == null) + { + columnMappings = new SortedSet(ViewColumnMappingComparer.Instance); + property.SetAnnotation(RelationalAnnotationNames.ViewColumnMappings, columnMappings); + } + + columnMappings.Add(columnMapping); + } - var columnMappings = property[RelationalAnnotationNames.TableColumnMappings] as SortedSet; - if (columnMappings == null) + var tableMappings = entityType[RelationalAnnotationNames.ViewMappings] as SortedSet; + if (tableMappings == null) { - columnMappings = new SortedSet(ColumnMappingComparer.Instance); - property.SetAnnotation(RelationalAnnotationNames.TableColumnMappings, columnMappings); + tableMappings = new SortedSet(ViewMappingComparer.Instance); + entityType.SetAnnotation(RelationalAnnotationNames.ViewMappings, tableMappings); } - columnMappings.Add(columnMapping); + tableMappings.Add(viewMapping); + view.EntityTypeMappings.Add(viewMapping); } + } - var tableMappings = entityType[RelationalAnnotationNames.TableMappings] as SortedSet; - if (tableMappings == null) - { - tableMappings = new SortedSet(TableMappingComparer.Instance); - entityType.SetAnnotation(RelationalAnnotationNames.TableMappings, tableMappings); - } + foreach (var table in tables.Values) + { + PopulateInternalForeignKeys(table); + } - tableMappings.Add(tableMapping); - table.EntityTypeMappings.Add(tableMapping); + foreach (var view in views.Values) + { + PopulateInternalForeignKeys(view); } - foreach (var table in tables.Values) + if (tables.Any()) { - SortedDictionary> internalForeignKeyMap = null; - SortedDictionary> referencingInternalForeignKeyMap = null; - foreach (var entityTypeMapping in table.EntityTypeMappings) + model.SetAnnotation(RelationalAnnotationNames.Tables, tables); + } + + if (views.Any()) + { + model.SetAnnotation(RelationalAnnotationNames.Views, views); + } + + return model; + } + + private static void PopulateInternalForeignKeys(TableBase table) + { + SortedDictionary> internalForeignKeyMap = null; + SortedDictionary> referencingInternalForeignKeyMap = null; + foreach (var entityTypeMapping in ((ITableBase)table).EntityTypeMappings) + { + var entityType = entityTypeMapping.EntityType; + var primaryKey = entityType.FindPrimaryKey(); + if (primaryKey == null) { - var entityType = entityTypeMapping.EntityType; - var primaryKey = entityType.FindPrimaryKey(); - if (primaryKey == null) - { - continue; - } + continue; + } - SortedSet internalForeignKeys = null; - foreach (var foreignKey in entityType.FindForeignKeys(primaryKey.Properties)) + SortedSet internalForeignKeys = null; + foreach (var foreignKey in entityType.FindForeignKeys(primaryKey.Properties)) + { + if (foreignKey.IsUnique + && foreignKey.PrincipalKey.IsPrimaryKey() + && !foreignKey.IsIntraHierarchical() + && ((ITableBase)table).EntityTypeMappings.Any(m => m.EntityType == foreignKey.PrincipalEntityType)) { - if (foreignKey.IsUnique - && foreignKey.PrincipalKey.IsPrimaryKey() - && !foreignKey.IsIntraHierarchical() - && table.EntityTypeMappings.Any(m => m.EntityType == foreignKey.PrincipalEntityType)) + if (internalForeignKeys == null) { - 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); + internalForeignKeys = new SortedSet(ForeignKeyComparer.Instance); } - } + internalForeignKeys.Add(foreignKey); - if (internalForeignKeys != null) - { - if (internalForeignKeyMap == null) + if (referencingInternalForeignKeyMap == null) { - internalForeignKeyMap = + referencingInternalForeignKeyMap = new SortedDictionary>(EntityTypePathComparer.Instance); - table.InternalForeignKeys = internalForeignKeyMap; } - internalForeignKeyMap[entityType] = internalForeignKeys; + 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 - && table.EntityTypeMappings.Any(m => !m.EntityType.IsSameHierarchy(entityType))) + if (internalForeignKeys != null) + { + if (internalForeignKeyMap == null) { - table.IsSplit = true; + internalForeignKeyMap = + new SortedDictionary>(EntityTypePathComparer.Instance); + table.InternalForeignKeys = internalForeignKeyMap; } + + internalForeignKeyMap[entityType] = internalForeignKeys; } - if (referencingInternalForeignKeyMap != null) + if (internalForeignKeys == null + && ((ITableBase)table).EntityTypeMappings.Any(m => !m.EntityType.IsSameHierarchy(entityType))) { - table.ReferencingInternalForeignKeys = referencingInternalForeignKeyMap; + table.IsSplit = true; } } - model.SetAnnotation(RelationalAnnotationNames.Tables, tables); - return model; + if (referencingInternalForeignKeyMap != null) + { + table.ReferencingInternalForeignKeys = referencingInternalForeignKeyMap; + } } } } diff --git a/src/EFCore.Relational/Metadata/Internal/Table.cs b/src/EFCore.Relational/Metadata/Internal/Table.cs index d5d076e50a1..3fb8f2065ec 100644 --- a/src/EFCore.Relational/Metadata/Internal/Table.cs +++ b/src/EFCore.Relational/Metadata/Internal/Table.cs @@ -15,7 +15,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 Table : Annotatable, ITable + public class Table : TableBase, ITable { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -24,24 +24,17 @@ public class Table : Annotatable, ITable /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public Table([NotNull] string name, [CanBeNull] string schema) + : base(name, 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); + 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 @@ -49,30 +42,11 @@ public Table([NotNull] string name, [CanBeNull] string schema) /// any release. You should only use it directly in your 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 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) @@ -115,13 +89,18 @@ IEnumerable ITableBase.EntityTypeMappings get => EntityTypeMappings; } - IEnumerable ITable.GetInternalForeignKeys(IEntityType entityType) + /// + IColumnBase ITableBase.FindColumn(string name) => FindColumn(name); + + /// + IEnumerable ITableBase.GetInternalForeignKeys(IEntityType entityType) => InternalForeignKeys != null && InternalForeignKeys.TryGetValue(entityType, out var foreignKeys) ? foreignKeys : null; - IEnumerable ITable.GetReferencingInternalForeignKeys(IEntityType entityType) + /// + IEnumerable ITableBase.GetReferencingInternalForeignKeys(IEntityType entityType) => ReferencingInternalForeignKeys != null && ReferencingInternalForeignKeys.TryGetValue(entityType, out var foreignKeys) ? foreignKeys diff --git a/src/EFCore.Relational/Metadata/Internal/TableBase.cs b/src/EFCore.Relational/Metadata/Internal/TableBase.cs new file mode 100644 index 00000000000..dc975e329a1 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/TableBase.cs @@ -0,0 +1,78 @@ +// 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 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 abstract class TableBase : Annotatable, ITableBase + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public TableBase([NotNull] string name, [CanBeNull] string schema) + { + Schema = schema; + Name = name; + } + + /// + public virtual string Schema { get; } + + /// + public virtual string Name { get; } + + /// + 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; } + + /// + IEnumerable ITableBase.EntityTypeMappings => throw new System.NotImplementedException(); + + /// + IEnumerable ITableBase.Columns => throw new System.NotImplementedException(); + + /// + IColumnBase ITableBase.FindColumn(string name) => throw new System.NotImplementedException(); + + /// + IEnumerable ITableBase.GetInternalForeignKeys(IEntityType entityType) + => InternalForeignKeys != null + && InternalForeignKeys.TryGetValue(entityType, out var foreignKeys) + ? foreignKeys + : null; + + /// + IEnumerable ITableBase.GetReferencingInternalForeignKeys(IEntityType entityType) + => ReferencingInternalForeignKeys != null + && ReferencingInternalForeignKeys.TryGetValue(entityType, out var foreignKeys) + ? foreignKeys + : null; + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/TableMapping.cs b/src/EFCore.Relational/Metadata/Internal/TableMapping.cs index ab4b3255b4b..01ac8a154c4 100644 --- a/src/EFCore.Relational/Metadata/Internal/TableMapping.cs +++ b/src/EFCore.Relational/Metadata/Internal/TableMapping.cs @@ -24,7 +24,7 @@ public class TableMapping : Annotatable, ITableMapping /// public TableMapping( [NotNull] IEntityType entityType, - [NotNull] ITable table, + [NotNull] Table table, bool includesDerivedTypes) { EntityType = entityType; diff --git a/src/EFCore.Relational/Metadata/Internal/View.cs b/src/EFCore.Relational/Metadata/Internal/View.cs new file mode 100644 index 00000000000..ddd443f3bde --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/View.cs @@ -0,0 +1,113 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +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 View : TableBase, IView + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public View([NotNull] string name, [CanBeNull] string schema) + : base(name, schema) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your 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(ViewMappingComparer.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 string ViewDefinition + => (string)EntityTypeMappings.Select(m => m.EntityType[RelationalAnnotationNames.ViewDefinition]).FirstOrDefault(d => d != null); + + /// + public virtual IViewColumn 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); + + /// + [DebuggerStepThrough] + IColumnBase ITableBase.FindColumn(string name) => FindColumn(name); + + /// + IEnumerable ITableBase.Columns + { + [DebuggerStepThrough] + get => Columns.Values; + } + + /// + IEnumerable IView.Columns + { + [DebuggerStepThrough] + get => Columns.Values; + } + + /// + IEnumerable IView.EntityTypeMappings + { + [DebuggerStepThrough] + get => EntityTypeMappings; + } + + /// + IEnumerable ITableBase.EntityTypeMappings + { + [DebuggerStepThrough] + get => EntityTypeMappings; + } + + /// + IEnumerable ITableBase.GetInternalForeignKeys(IEntityType entityType) + => InternalForeignKeys != null + && InternalForeignKeys.TryGetValue(entityType, out var foreignKeys) + ? foreignKeys + : null; + + /// + IEnumerable ITableBase.GetReferencingInternalForeignKeys(IEntityType entityType) + => ReferencingInternalForeignKeys != null + && ReferencingInternalForeignKeys.TryGetValue(entityType, out var foreignKeys) + ? foreignKeys + : null; + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/ViewColumn.cs b/src/EFCore.Relational/Metadata/Internal/ViewColumn.cs new file mode 100644 index 00000000000..ccc542be2b5 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/ViewColumn.cs @@ -0,0 +1,82 @@ +// 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 ViewColumn : Annotatable, IViewColumn + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public ViewColumn([NotNull] string name, [NotNull] string type, [NotNull] View view) + { + Name = name; + Type = type; + View = view; + } + + /// + public virtual string Name { get; } + + /// + public virtual IView View { 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(ViewColumnMappingComparer.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 IViewColumn.PropertyMappings + { + [DebuggerStepThrough] + get => PropertyMappings; + } + + /// + IEnumerable IColumnBase.PropertyMappings + { + [DebuggerStepThrough] + get => PropertyMappings; + } + + /// + ITableBase IColumnBase.Table + { + [DebuggerStepThrough] + get => View; + } + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/ViewColumnExtensions.cs b/src/EFCore.Relational/Metadata/Internal/ViewColumnExtensions.cs new file mode 100644 index 00000000000..b6465babec9 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/ViewColumnExtensions.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 ViewColumnExtensions + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your 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 IViewColumn 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/ViewColumnMapping.cs b/src/EFCore.Relational/Metadata/Internal/ViewColumnMapping.cs new file mode 100644 index 00000000000..9591214327f --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/ViewColumnMapping.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 ViewColumnMapping : Annotatable, IViewColumnMapping + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public ViewColumnMapping( + [NotNull] IProperty property, + [NotNull] ViewColumn column, + [NotNull] RelationalTypeMapping typeMapping, + [NotNull] ViewMapping viewMapping) + { + Property = property; + Column = column; + TypeMapping = typeMapping; + ViewMapping = viewMapping; + } + + /// + public virtual IProperty Property { get; } + + /// + public virtual IViewColumn Column { get; } + + /// + public virtual RelationalTypeMapping TypeMapping { get; } + + /// + public virtual IViewMapping ViewMapping { 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 => ViewMapping; + } + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/ViewColumnMappingComparer.cs b/src/EFCore.Relational/Metadata/Internal/ViewColumnMappingComparer.cs new file mode 100644 index 00000000000..ec1bf6ea2fc --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/ViewColumnMappingComparer.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 ViewColumnMappingComparer : IEqualityComparer, IComparer + { + private ViewColumnMappingComparer() + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your 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 ViewColumnMappingComparer Instance = new ViewColumnMappingComparer(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your 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(IViewColumnMapping x, IViewColumnMapping 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(IViewColumnMapping x, IViewColumnMapping 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(IViewColumnMapping 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/ViewColumnMappingExtensions.cs b/src/EFCore.Relational/Metadata/Internal/ViewColumnMappingExtensions.cs new file mode 100644 index 00000000000..c4ab203da93 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/ViewColumnMappingExtensions.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 ViewColumnMappingExtensions + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your 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 IViewColumnMapping columnMapping, + MetadataDebugStringOptions options, + [NotNull] string indent = "") + { + var builder = new StringBuilder(); + + builder.Append(indent); + + var singleLine = (options & MetadataDebugStringOptions.SingleLine) != 0; + if (singleLine) + { + builder.Append($"ViewColumnMapping: "); + } + + 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/ViewExtensions.cs b/src/EFCore.Relational/Metadata/Internal/ViewExtensions.cs new file mode 100644 index 00000000000..a208e6041cb --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/ViewExtensions.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.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 ViewExtensions + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your 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 IView view, + MetadataDebugStringOptions options, + [NotNull] string indent = "") + { + var builder = new StringBuilder(); + + builder + .Append(indent) + .Append("View: "); + + if (view.Schema != null) + { + builder + .Append(view.Schema) + .Append("."); + } + + builder.Append(view.Name); + + if ((options & MetadataDebugStringOptions.SingleLine) == 0) + { + var mappings = view.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 = view.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(view.AnnotationsToDebugString(indent: indent + " ")); + } + } + + return builder.ToString(); + } + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/ViewMapping.cs b/src/EFCore.Relational/Metadata/Internal/ViewMapping.cs new file mode 100644 index 00000000000..c62afee2b35 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/ViewMapping.cs @@ -0,0 +1,82 @@ +// 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 ViewMapping : Annotatable, IViewMapping + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public ViewMapping( + [NotNull] IEntityType entityType, + [NotNull] View view, + bool includesDerivedTypes) + { + EntityType = entityType; + View = view; + IncludesDerivedTypes = includesDerivedTypes; + } + + /// + public virtual IEntityType EntityType { get; } + + /// + public virtual IView View { 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 ColumnMappings { get; } + = new SortedSet(ViewColumnMappingComparer.Instance); + + /// + public virtual bool IncludesDerivedTypes { 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); + + /// + ITableBase ITableMappingBase.Table + { + [DebuggerStepThrough] + get => View; + } + + /// + IEnumerable IViewMapping.ColumnMappings + { + [DebuggerStepThrough] + get => ColumnMappings; + } + + /// + IEnumerable ITableMappingBase.ColumnMappings + { + [DebuggerStepThrough] + get => ColumnMappings; + } + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/ViewMappingComparer.cs b/src/EFCore.Relational/Metadata/Internal/ViewMappingComparer.cs new file mode 100644 index 00000000000..a3d230cbff2 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/ViewMappingComparer.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 ViewMappingComparer : IEqualityComparer, IComparer + { + private ViewMappingComparer() + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your 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 ViewMappingComparer Instance = new ViewMappingComparer(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your 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(IViewMapping x, IViewMapping y) + { + var result = EntityTypePathComparer.Instance.Compare(x.EntityType, y.EntityType); + if (result != 0) + { + return result; + } + + result = StringComparer.Ordinal.Compare(x.View.Name, y.View.Name); + if (result != 0) + { + return result; + } + + result = StringComparer.Ordinal.Compare(x.View.Schema, y.View.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) => ViewColumnMappingComparer.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(IViewMapping x, IViewMapping y) + => x.EntityType == y.EntityType + && x.View == y.View + && 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(IViewMapping obj) + { + var hashCode = new HashCode(); + hashCode.Add(obj.EntityType, EntityTypePathComparer.Instance); + hashCode.Add(obj.View.Name); + hashCode.Add(obj.View.Schema); + foreach (var columnMapping in obj.ColumnMappings) + { + hashCode.Add(columnMapping, ViewColumnMappingComparer.Instance); + } + hashCode.Add(obj.IncludesDerivedTypes); + return hashCode.ToHashCode(); + } + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/ViewMappingExtensions.cs b/src/EFCore.Relational/Metadata/Internal/ViewMappingExtensions.cs new file mode 100644 index 00000000000..72dd61df650 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/ViewMappingExtensions.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 ViewMappingExtensions + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your 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 IViewMapping viewMapping, + MetadataDebugStringOptions options, + [NotNull] string indent = "") + { + var builder = new StringBuilder(); + + builder.Append(indent); + + var singleLine = (options & MetadataDebugStringOptions.SingleLine) != 0; + if (singleLine) + { + builder.Append($"ViewMapping: "); + } + + builder.Append(viewMapping.EntityType.Name).Append(" - "); + + builder.Append(viewMapping.Table.Name); + + if (viewMapping.IncludesDerivedTypes) + { + builder.Append($" IncludesDerivedTypes"); + } + + if (!singleLine && + (options & MetadataDebugStringOptions.IncludeAnnotations) != 0) + { + builder.Append(viewMapping.AnnotationsToDebugString(indent + " ")); + } + + return builder.ToString(); + } + } +} diff --git a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs index 7072d918d23..a941404e648 100644 --- a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs +++ b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs @@ -113,5 +113,20 @@ public static class RelationalAnnotationNames /// The name for column mappings annotations. /// public const string TableColumnMappings = Prefix + "TableColumnMappings"; + + /// + /// The name for tables annotation. + /// + public const string Views = Prefix + "Views"; + + /// + /// The name for table mappings annotations. + /// + public const string ViewMappings = Prefix + "ViewMappings"; + + /// + /// The name for column mappings annotations. + /// + public const string ViewColumnMappings = Prefix + "ViewColumnMappings"; } } diff --git a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs index ead09f2a687..36d57e4f5cf 100644 --- a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs +++ b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs @@ -580,15 +580,15 @@ protected virtual IEnumerable Diff( var sourceMigrationsAnnotations = MigrationsAnnotations.For(GetRootType(source)).ToList(); var targetMigrationsAnnotations = MigrationsAnnotations.For(GetRootType(target)).ToList(); - if (source.GetComment() != target.GetComment() + if (source.Comment != target.Comment || HasDifferences(sourceMigrationsAnnotations, targetMigrationsAnnotations)) { var alterTableOperation = new AlterTableOperation { Name = target.Name, Schema = target.Schema, - Comment = target.GetComment(), - OldTable = { Comment = source.GetComment() } + Comment = target.Comment, + OldTable = { Comment = source.Comment } }; alterTableOperation.AddAnnotations(targetMigrationsAnnotations); @@ -601,7 +601,7 @@ protected virtual IEnumerable Diff( 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)); + .Concat(Diff(source.CheckConstraints, target.CheckConstraints, diffContext)); foreach (var operation in operations) { @@ -630,7 +630,7 @@ protected virtual IEnumerable Add( { Schema = target.Schema, Name = target.Name, - Comment = target.GetComment() + Comment = target.Comment }; createTableOperation.AddAnnotations(MigrationsAnnotations.For(entityType)); @@ -646,7 +646,7 @@ protected virtual IEnumerable Add( GetKeys(target).Where(k => !k.IsPrimaryKey()).SelectMany(k => Add(k, diffContext)) .Cast()); createTableOperation.CheckConstraints.AddRange( - target.GetCheckConstraints().SelectMany(c => Add(c, diffContext)) + target.CheckConstraints.SelectMany(c => Add(c, diffContext)) .Cast()); foreach (var targetMapping in target.EntityTypeMappings) diff --git a/src/EFCore.Relational/Query/SqlExpressionFactory.cs b/src/EFCore.Relational/Query/SqlExpressionFactory.cs index 8ee8fe6a993..d8dad55a6fd 100644 --- a/src/EFCore.Relational/Query/SqlExpressionFactory.cs +++ b/src/EFCore.Relational/Query/SqlExpressionFactory.cs @@ -751,7 +751,7 @@ public virtual SelectExpression Select(IEntityType entityType, string sql, Expre private void AddConditions( SelectExpression selectExpression, IEntityType entityType, - ICollection sharingTypes = null, + ITableBase table = null, bool skipJoins = false) { if (entityType.FindPrimaryKey() == null) @@ -760,54 +760,46 @@ private void AddConditions( } else { - sharingTypes ??= new HashSet( - entityType.Model.GetEntityTypes() - .Where( - et => et.FindPrimaryKey() != null - && et.GetTableName() == entityType.GetTableName() - && et.GetSchema() == entityType.GetSchema())); - - if (sharingTypes.Count > 0) + var tableMappings = entityType.GetViewOrTableMappings(); + if (tableMappings == null) { - var discriminatorAdded = AddDiscriminatorCondition(selectExpression, entityType); + return; + } - var linkingFks = entityType.GetRootType().FindForeignKeys(entityType.FindPrimaryKey().Properties) - .Where( - fk => fk.PrincipalKey.IsPrimaryKey() - && fk.PrincipalEntityType != entityType - && sharingTypes.Contains(fk.PrincipalEntityType)) - .ToList(); + table ??= tableMappings.Single().Table; + var discriminatorAdded = AddDiscriminatorCondition(selectExpression, entityType); - if (linkingFks.Count > 0) + var linkingFks = table.GetInternalForeignKeys(entityType); + if (linkingFks != null + && linkingFks.Any()) + { + if (!discriminatorAdded) { - if (!discriminatorAdded) - { - AddOptionalDependentConditions(selectExpression, entityType, sharingTypes); - } + AddOptionalDependentConditions(selectExpression, entityType, table); + } - if (!skipJoins) - { - var first = true; + if (!skipJoins) + { + var first = true; - foreach (var foreignKey in linkingFks) + foreach (var foreignKey in linkingFks) + { + if (!(entityType.FindOwnership() == foreignKey + && foreignKey.PrincipalEntityType.BaseType == null)) { - if (!(entityType.FindOwnership() == foreignKey - && foreignKey.PrincipalEntityType.BaseType == null)) + var otherSelectExpression = first + ? selectExpression + : new SelectExpression(entityType); + + AddInnerJoin(otherSelectExpression, foreignKey, table, skipInnerJoins: false); + + if (first) + { + first = false; + } + else { - var otherSelectExpression = first - ? selectExpression - : new SelectExpression(entityType); - - AddInnerJoin(otherSelectExpression, foreignKey, sharingTypes, skipInnerJoins: false); - - if (first) - { - first = false; - } - else - { - selectExpression.ApplyUnion(otherSelectExpression, distinct: true); - } + selectExpression.ApplyUnion(otherSelectExpression, distinct: true); } } } @@ -817,9 +809,9 @@ private void AddConditions( } private void AddInnerJoin( - SelectExpression selectExpression, IForeignKey foreignKey, ICollection sharingTypes, bool skipInnerJoins) + SelectExpression selectExpression, IForeignKey foreignKey, ITableBase table, bool skipInnerJoins) { - var joinPredicate = GenerateJoinPredicate(selectExpression, foreignKey, sharingTypes, skipInnerJoins, out var innerSelect); + var joinPredicate = GenerateJoinPredicate(selectExpression, foreignKey, table, skipInnerJoins, out var innerSelect); selectExpression.AddInnerJoin(innerSelect, joinPredicate, null); } @@ -827,7 +819,7 @@ private void AddInnerJoin( private SqlExpression GenerateJoinPredicate( SelectExpression selectExpression, IForeignKey foreignKey, - ICollection sharingTypes, + ITableBase table, bool skipInnerJoins, out SelectExpression innerSelect) { @@ -840,7 +832,7 @@ private SqlExpression GenerateJoinPredicate( AddConditions( innerSelect, outerIsPrincipal ? foreignKey.DeclaringEntityType : foreignKey.PrincipalEntityType, - sharingTypes, + table, skipInnerJoins); var innerEntityProjection = GetMappedEntityProjectionExpression(innerSelect); @@ -886,7 +878,7 @@ private bool AddDiscriminatorCondition(SelectExpression selectExpression, IEntit } private void AddOptionalDependentConditions( - SelectExpression selectExpression, IEntityType entityType, ICollection sharingTypes) + SelectExpression selectExpression, IEntityType entityType, ITableBase table) { SqlExpression predicate = null; var requiredNonPkProperties = entityType.GetProperties().Where(p => !p.IsNullable && !p.IsPrimaryKey()).ToList(); @@ -935,10 +927,10 @@ private void AddOptionalDependentConditions( { var otherSelectExpression = new SelectExpression(entityType); - var sameTable = sharingTypes.Contains(referencingFk.DeclaringEntityType); + var sameTable = table.GetInternalForeignKeys(referencingFk.DeclaringEntityType) != null; AddInnerJoin( otherSelectExpression, referencingFk, - sameTable ? sharingTypes : null, + sameTable ? table : null, skipInnerJoins: sameTable); selectExpression.ApplyUnion(otherSelectExpression, distinct: true); } diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index e3dcf2f418e..68f247d2aa3 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -65,11 +65,7 @@ internal SelectExpression( } internal SelectExpression(IEntityType entityType) - : this( - entityType, new TableExpression( - entityType.GetTableName(), - entityType.GetSchema(), - entityType.GetTableName().Substring(0, 1).ToLower())) + : this(entityType, new TableExpression(entityType.GetViewOrTableMappings().Single().Table)) { } @@ -78,7 +74,7 @@ internal SelectExpression(IEntityType entityType, string sql, Expression argumen entityType, new FromSqlExpression( sql, arguments, - (entityType.GetTableName() ?? entityType.GetViewName() ?? entityType.ShortName()).Substring(0, 1).ToLower())) + (entityType.GetViewOrTableMappings()?.Single().Table.Name ?? entityType.ShortName()).Substring(0, 1).ToLower())) { } diff --git a/src/EFCore.Relational/Query/SqlExpressions/TableExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/TableExpression.cs index 427897a1806..0633bc022fc 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/TableExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/TableExpression.cs @@ -3,6 +3,7 @@ using System; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions @@ -10,11 +11,11 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions // Class is sealed because there are no public/protected constructors. Can be unsealed if this is changed. public sealed class TableExpression : TableExpressionBase { - internal TableExpression(string name, string schema, [NotNull] string alias) - : base(alias) + internal TableExpression([NotNull] ITableBase table) + : base(table.Name.Substring(0, 1).ToLower()) { - Name = name; - Schema = schema; + Name = table.Name; + Schema = table.Schema; } public override void Print(ExpressionPrinter expressionPrinter) diff --git a/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs b/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs index 4859c71764d..8ac6b44ccf3 100644 --- a/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs +++ b/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs @@ -180,9 +180,8 @@ protected virtual IEnumerable CreateModificationCommands( } var entityType = entry.EntityType; - var table = entityType.GetTableName(); - var schema = entityType.GetSchema(); - var tableKey = (table, schema); + var table = entityType.GetTableMappings().Single().Table; + var tableKey = (table.Name, table.Schema); ModificationCommand command; var isMainEntry = true; @@ -208,7 +207,7 @@ protected virtual IEnumerable CreateModificationCommands( else { command = new ModificationCommand( - table, schema, generateParameterName, _sensitiveLoggingEnabled, comparer: null); + table.Name, table.Schema, generateParameterName, _sensitiveLoggingEnabled, comparer: null); } command.AddEntry(entry, isMainEntry); diff --git a/src/EFCore.Relational/Update/Internal/SharedTableEntryMap.cs b/src/EFCore.Relational/Update/Internal/SharedTableEntryMap.cs index c54ff082adb..11b15754a55 100644 --- a/src/EFCore.Relational/Update/Internal/SharedTableEntryMap.cs +++ b/src/EFCore.Relational/Update/Internal/SharedTableEntryMap.cs @@ -39,7 +39,7 @@ public SharedTableEntryMap( _table = table; _updateAdapter = updateAdapter; _createElement = createElement; - _comparer = new EntryComparer(IsMainEntityType); + _comparer = new EntryComparer(table); } /// @@ -176,17 +176,17 @@ private void AddAllDependentsInclusive(IUpdateEntry entry, List en private sealed class EntryComparer : IComparer { - private readonly Func _isMain; + private readonly ITable _table; - public EntryComparer(Func isMain) + public EntryComparer(ITable table) { - _isMain = isMain; + _table = table; } public int Compare(IUpdateEntry x, IUpdateEntry y) - => _isMain(x.EntityType) + => _table.GetInternalForeignKeys(x.EntityType) == null ? -1 - : _isMain(y.EntityType) + : _table.GetInternalForeignKeys(y.EntityType) == null ? 1 : StringComparer.Ordinal.Compare(x.EntityType.Name, y.EntityType.Name); } diff --git a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs index 48aaf6c33ff..1cb8577df0c 100644 --- a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs +++ b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs @@ -243,7 +243,7 @@ public virtual void Model_annotations_are_stored_in_snapshot() .HasAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.IdentityColumn);"), o => { - Assert.Equal(7, o.GetAnnotations().Count()); + Assert.Equal(6, o.GetAnnotations().Count()); Assert.Equal("AnnotationValue", o["AnnotationName"]); }); } @@ -266,7 +266,7 @@ public virtual void Model_default_schema_annotation_is_stored_in_snapshot_as_flu .HasAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.IdentityColumn);"), o => { - Assert.Equal(5, o.GetAnnotations().Count()); + Assert.Equal(4, o.GetAnnotations().Count()); Assert.Equal("AnnotationValue", o["AnnotationName"]); Assert.Equal("DefaultSchema", o[RelationalAnnotationNames.DefaultSchema]); }); @@ -352,7 +352,7 @@ public virtual void Sequence_is_stored_in_snapshot_as_annotations() .HasAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.IdentityColumn);"), o => { - Assert.Equal(4, o.GetAnnotations().Count()); + Assert.Equal(3, o.GetAnnotations().Count()); }); } diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs index 021d70c40a3..861a7a2307d 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs @@ -14,25 +14,13 @@ namespace Microsoft.EntityFrameworkCore.Metadata public class RelationalModelTest { [ConditionalFact] - public void Can_use_relational_model() + public void Can_use_relational_model_with_tables() { - 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(); + var model = CreateTestModel(); Assert.Equal(6, model.GetEntityTypes().Count()); + Assert.Equal(2, model.GetTables().Count()); + Assert.Empty(model.GetViews()); var orderType = model.FindEntityType(typeof(Order)); var orderMapping = orderType.GetTableMappings().Single(); @@ -42,6 +30,7 @@ public void Can_use_relational_model() orderMapping.ColumnMappings.Select(m => m.Property.Name)); var ordersTable = orderMapping.Table; + Assert.Same(ordersTable, model.FindTable(ordersTable.Name, ordersTable.Schema)); Assert.Equal( new[] { "OrderDetails.BillingAddress#Address", "OrderDetails.ShippingAddress#Address", nameof(Order), nameof(OrderDetails) }, ordersTable.EntityTypeMappings.Select(m => m.EntityType.DisplayName())); @@ -70,7 +59,7 @@ public void Can_use_relational_model() var orderDetailsOwnership = orderType.FindNavigation(nameof(Order.Details)).ForeignKey; var orderDetailsType = orderDetailsOwnership.DeclaringEntityType; - Assert.Single(orderDetailsType.GetTableMappings()); + Assert.Same(ordersTable, orderDetailsType.GetTableMappings().Single().Table); Assert.Equal(ordersTable.GetReferencingInternalForeignKeys(orderType), ordersTable.GetInternalForeignKeys(orderDetailsType)); var orderDetailsDate = orderDetailsType.FindProperty(nameof(OrderDetails.OrderDate)); @@ -85,10 +74,112 @@ public void Can_use_relational_model() Assert.Same(ordersTable, orderDateColumn.Table); var customerType = model.FindEntityType(typeof(Customer)); - Assert.Single(customerType.GetTableMappings()); + var customerTable = customerType.GetTableMappings().Single().Table; + Assert.Equal("Customer", customerTable.Name); + + var specialCustomerType = model.FindEntityType(typeof(SpecialCustomer)); + Assert.Same(customerTable, specialCustomerType.GetTableMappings().Single().Table); + } + + [ConditionalFact] + public void Can_use_relational_model_with_views() + { + var model = CreateTestModel(mapToViews: true); + + Assert.Equal(6, model.GetEntityTypes().Count()); + Assert.Equal(2, model.GetViews().Count()); + Assert.Empty(model.GetTables()); + + var orderType = model.FindEntityType(typeof(Order)); + var orderMapping = orderType.GetViewMappings().Single(); + Assert.Null(orderType.GetTableMappings()); + Assert.Same(orderType.GetViewMappings(), orderType.GetViewOrTableMappings()); + Assert.True(orderMapping.IncludesDerivedTypes); + Assert.Equal( + new[] { nameof(Order.CustomerId), nameof(Order.OrderDate), nameof(Order.OrderId) }, + orderMapping.ColumnMappings.Select(m => m.Property.Name)); + + var ordersView = orderMapping.View; + Assert.Same(ordersView, model.FindView(ordersView.Name, ordersView.Schema)); + Assert.Equal( + new[] { "OrderDetails.BillingAddress#Address", "OrderDetails.ShippingAddress#Address", nameof(Order), nameof(OrderDetails) }, + ordersView.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) + }, + ordersView.Columns.Select(m => m.Name)); + Assert.Equal("OrderView", ordersView.Name); + Assert.Null(ordersView.Schema); + Assert.Null(ordersView.ViewDefinition); + + var orderDate = orderType.FindProperty(nameof(Order.OrderDate)); + Assert.False(orderDate.IsColumnNullable()); + + var orderDateMapping = orderDate.GetViewColumnMappings().Single(); + Assert.NotNull(orderDateMapping.TypeMapping); + Assert.Equal("default_datetime_mapping", orderDateMapping.TypeMapping.StoreType); + Assert.Same(orderMapping, orderDateMapping.ViewMapping); + + var orderDetailsOwnership = orderType.FindNavigation(nameof(Order.Details)).ForeignKey; + var orderDetailsType = orderDetailsOwnership.DeclaringEntityType; + Assert.Same(ordersView, orderDetailsType.GetViewMappings().Single().View); + Assert.Equal(ordersView.GetReferencingInternalForeignKeys(orderType), ordersView.GetInternalForeignKeys(orderDetailsType)); + + var orderDetailsDate = orderDetailsType.FindProperty(nameof(OrderDetails.OrderDate)); + Assert.True(orderDetailsDate.IsColumnNullable()); + + var orderDateColumn = orderDateMapping.Column; + Assert.Same(orderDateColumn, ordersView.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(ordersView, orderDateColumn.Table); + + var customerType = model.FindEntityType(typeof(Customer)); + var customerView = customerType.GetViewMappings().Single().Table; + Assert.Equal("CustomerView", customerView.Name); var specialCustomerType = model.FindEntityType(typeof(SpecialCustomer)); - Assert.Single(specialCustomerType.GetTableMappings()); + Assert.Same(customerView, specialCustomerType.GetViewMappings().Single().Table); + } + + private IModel CreateTestModel(bool mapToViews = false) + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder.Entity(cb => + { + if (mapToViews) + { + cb.ToView("CustomerView"); + } + }); + 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); + }); + + if (mapToViews) + { + ob.ToView("OrderView"); + } + }); + + var model = modelBuilder.FinalizeModel(); + return model; } protected virtual ModelBuilder CreateConventionModelBuilder() => RelationalTestHelpers.Instance.CreateConventionBuilder();