From 35cc430054e2fcc35e8280887c584c8ce3fde1cd Mon Sep 17 00:00:00 2001 From: AndriySvyryd Date: Mon, 24 Feb 2020 18:04:46 -0800 Subject: [PATCH] Allow to map an entity type to both a table and a view. Fixes #17270 Fixes #15671 --- .../Design/CSharpSnapshotGenerator.cs | 6 + .../Internal/CSharpDbContextGenerator.cs | 48 ++++- .../Internal/CSharpEntityTypeGenerator.cs | 3 +- .../RelationalEntityTypeBuilderExtensions.cs | 6 +- .../RelationalEntityTypeExtensions.cs | 162 +++++++++++++--- .../Extensions/RelationalKeyExtensions.cs | 4 +- .../RelationalPropertyBuilderExtensions.cs | 67 +++++++ .../RelationalPropertyExtensions.cs | 175 ++++++++++++++++-- .../RelationalConventionSetBuilder.cs | 1 + .../RelationalValueGenerationConvention.cs | 4 +- .../Conventions/SharedTableConvention.cs | 4 +- .../TableNameFromDbSetConvention.cs | 44 +++-- .../Metadata/Internal/RelationalModel.cs | 11 +- .../Internal/RelationalPropertyExtensions.cs | 52 +++++- .../Metadata/Internal/StoreObjectType.cs | 38 ++++ .../Internal/ViewColumnMappingComparer.cs | 2 +- .../Metadata/RelationalAnnotationNames.cs | 15 ++ .../Internal/MigrationsModelDiffer.cs | 3 +- .../Extensions/SqlServerKeyExtensions.cs | 2 +- .../Extensions/SqlServerPropertyExtensions.cs | 2 +- .../Internal/CSharpEntityTypeGeneratorTest.cs | 6 +- .../RelationalScaffoldingModelFactoryTest.cs | 3 +- .../RelationalMetadataExtensionsTest.cs | 25 ++- .../Metadata/RelationalModelTest.cs | 150 +++++++++------ 24 files changed, 679 insertions(+), 154 deletions(-) create mode 100644 src/EFCore.Relational/Metadata/Internal/StoreObjectType.cs diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index bb8217ab7d8..3f80b2fa144 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -438,6 +438,12 @@ protected virtual void GeneratePropertyAnnotations([NotNull] IProperty property, nameof(RelationalPropertyBuilderExtensions.HasColumnName), stringBuilder); + GenerateFluentApiForAnnotation( + ref annotations, + RelationalAnnotationNames.ViewColumnName, + nameof(RelationalPropertyBuilderExtensions.HasViewColumnName), + stringBuilder); + stringBuilder .AppendLine() .Append(".") diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs index 75c5d8fdba8..18421a9900e 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs @@ -364,15 +364,16 @@ private void GenerateEntityType(IEntityType entityType, bool useDataAnnotations) var annotations = entityType.GetAnnotations().ToList(); RemoveAnnotation(ref annotations, CoreAnnotationNames.ConstructorBinding); RemoveAnnotation(ref annotations, RelationalAnnotationNames.TableName); - RemoveAnnotation(ref annotations, RelationalAnnotationNames.Comment); RemoveAnnotation(ref annotations, RelationalAnnotationNames.Schema); + RemoveAnnotation(ref annotations, RelationalAnnotationNames.ViewName); + RemoveAnnotation(ref annotations, RelationalAnnotationNames.ViewSchema); RemoveAnnotation(ref annotations, RelationalAnnotationNames.TableMappings); RemoveAnnotation(ref annotations, RelationalAnnotationNames.ViewMappings); RemoveAnnotation(ref annotations, ScaffoldingAnnotationNames.DbSetName); - + RemoveAnnotation(ref annotations, RelationalAnnotationNames.Comment); RemoveAnnotation(ref annotations, RelationalAnnotationNames.ViewDefinition); - var isView = entityType.FindAnnotation(RelationalAnnotationNames.ViewDefinition) != null; - if (!useDataAnnotations || isView) + + if (!useDataAnnotations || entityType.GetViewName() != null) { GenerateTableName(entityType); } @@ -533,9 +534,7 @@ private void GenerateTableName(IEntityType entityType) var explicitSchema = schema != null && schema != defaultSchema; var explicitTable = explicitSchema || tableName != null && tableName != entityType.GetDbSetName(); - - var isView = entityType.FindAnnotation(RelationalAnnotationNames.ViewDefinition) != null; - if (explicitTable || isView) + if (explicitTable) { var parameterString = _code.Literal(tableName); if (explicitSchema) @@ -545,7 +544,29 @@ private void GenerateTableName(IEntityType entityType) var lines = new List { - $".{(isView ? nameof(RelationalEntityTypeBuilderExtensions.ToView) : nameof(RelationalEntityTypeBuilderExtensions.ToTable))}({parameterString})" + $".{nameof(RelationalEntityTypeBuilderExtensions.ToTable)}({parameterString})" + }; + + AppendMultiLineFluentApi(entityType, lines); + } + + var viewName = entityType.GetViewName(); + var viewSchema = entityType.GetViewSchema(); + + var explicitViewSchema = viewSchema != null && viewSchema != defaultSchema; + var explicitViewTable = explicitViewSchema || viewName != null; + + if (explicitViewTable) + { + var parameterString = _code.Literal(viewName); + if (explicitViewSchema) + { + parameterString += ", " + _code.Literal(viewSchema); + } + + var lines = new List + { + $".{nameof(RelationalEntityTypeBuilderExtensions.ToView)}({parameterString})" }; AppendMultiLineFluentApi(entityType, lines); @@ -614,6 +635,7 @@ private void GenerateProperty(IProperty property, bool useDataAnnotations) RemoveAnnotation(ref annotations, CoreAnnotationNames.TypeMapping); RemoveAnnotation(ref annotations, CoreAnnotationNames.Unicode); RemoveAnnotation(ref annotations, RelationalAnnotationNames.ColumnName); + RemoveAnnotation(ref annotations, RelationalAnnotationNames.ViewColumnName); RemoveAnnotation(ref annotations, RelationalAnnotationNames.ColumnType); RemoveAnnotation(ref annotations, RelationalAnnotationNames.DefaultValue); RemoveAnnotation(ref annotations, RelationalAnnotationNames.DefaultValueSql); @@ -643,6 +665,16 @@ private void GenerateProperty(IProperty property, bool useDataAnnotations) $"({_code.Literal(columnName)})"); } + var viewColumnName = property.GetViewColumnName(); + + if (viewColumnName != null + && viewColumnName != columnName) + { + lines.Add( + $".{nameof(RelationalPropertyBuilderExtensions.HasViewColumnName)}" + + $"({_code.Literal(columnName)})"); + } + var columnType = property.GetConfiguredColumnType(); if (columnType != null) diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs index 58aeff299ee..2f0470c9f34 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs @@ -152,9 +152,8 @@ private void GenerateTableAttribute(IEntityType entityType) var defaultSchema = entityType.Model.GetDefaultSchema(); var schemaParameterNeeded = schema != null && schema != defaultSchema; - var isView = entityType.FindAnnotation(RelationalAnnotationNames.ViewDefinition) != null; + var isView = entityType.GetViewName() != null; var tableAttributeNeeded = !isView && (schemaParameterNeeded || tableName != null && tableName != entityType.GetDbSetName()); - if (tableAttributeNeeded) { var tableAttribute = new AttributeWriter(nameof(TableAttribute)); diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs index c7524b9a479..cdd3def5a02 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs @@ -277,7 +277,7 @@ public static EntityTypeBuilder ToView( Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); Check.NullButNotEmpty(name, nameof(name)); - entityTypeBuilder.Metadata.SetTableName(name); + entityTypeBuilder.Metadata.SetViewName(name); entityTypeBuilder.Metadata.SetAnnotation(RelationalAnnotationNames.ViewDefinition, null); return entityTypeBuilder; @@ -312,8 +312,8 @@ public static EntityTypeBuilder ToView( Check.NullButNotEmpty(name, nameof(name)); Check.NullButNotEmpty(schema, nameof(schema)); - entityTypeBuilder.Metadata.SetTableName(name); - entityTypeBuilder.Metadata.SetSchema(schema); + entityTypeBuilder.Metadata.SetViewName(name); + entityTypeBuilder.Metadata.SetViewSchema(schema); entityTypeBuilder.Metadata.SetAnnotation(RelationalAnnotationNames.ViewDefinition, null); return entityTypeBuilder; diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs index eacf8d04447..39a94cb96f0 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs @@ -1,6 +1,7 @@ // 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 JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -21,10 +22,24 @@ public static class RelationalEntityTypeExtensions /// /// The entity type to get the table name for. /// The name of the table to which the entity type is mapped. - public static string GetTableName([NotNull] this IEntityType entityType) => - entityType.BaseType != null - ? entityType.GetRootType().GetTableName() - : (string)entityType[RelationalAnnotationNames.TableName] ?? GetDefaultTableName(entityType); + public static string GetTableName([NotNull] this IEntityType entityType) + { + if (entityType.BaseType != null) + { + return entityType.GetRootType().GetTableName(); + } + + var nameAnnotation = entityType.FindAnnotation(RelationalAnnotationNames.TableName); + if (nameAnnotation != null) + { + return (string)nameAnnotation.Value; + } + + return ((entityType as IConventionEntityType)?.GetViewNameConfigurationSource() == null + && ((entityType as IConventionEntityType)?.GetDefiningQueryConfigurationSource() == null) + ? GetDefaultTableName(entityType) + : null); + } /// /// Returns the default table name that would be used for this entity type. @@ -33,11 +48,6 @@ 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) @@ -45,11 +55,16 @@ public static string GetDefaultTableName([NotNull] this IEntityType entityType) return ownership.PrincipalEntityType.GetTableName(); } - return Uniquifier.Truncate( - entityType.HasDefiningNavigation() - ? $"{entityType.DefiningEntityType.GetTableName()}_{entityType.DefiningNavigationName}" - : entityType.ShortName(), - entityType.Model.GetMaxIdentifierLength()); + var name = entityType.ShortName(); + if (entityType.HasDefiningNavigation()) + { + var definingTypeName = entityType.DefiningEntityType.GetTableName(); + name = definingTypeName != null + ? $"{definingTypeName}_{entityType.DefiningNavigationName}" + : $"{entityType.DefiningNavigationName}_{name}"; + } + + return Uniquifier.Truncate(name, entityType.Model.GetMaxIdentifierLength()); } /// @@ -58,7 +73,7 @@ public static string GetDefaultTableName([NotNull] this IEntityType entityType) /// The entity type to set the table name for. /// The name to set. public static void SetTableName([NotNull] this IMutableEntityType entityType, [CanBeNull] string name) - => entityType.SetOrRemoveAnnotation( + => entityType.SetAnnotation( RelationalAnnotationNames.TableName, Check.NullButNotEmpty(name, nameof(name))); @@ -70,7 +85,7 @@ public static void SetTableName([NotNull] this IMutableEntityType entityType, [C /// Indicates whether the configuration was specified using a data annotation. public static void SetTableName( [NotNull] this IConventionEntityType entityType, [CanBeNull] string name, bool fromDataAnnotation = false) - => entityType.SetOrRemoveAnnotation( + => entityType.SetAnnotation( RelationalAnnotationNames.TableName, Check.NullButNotEmpty(name, nameof(name)), fromDataAnnotation); @@ -97,7 +112,7 @@ public static string GetSchema([NotNull] this IEntityType entityType) => /// /// Returns the default database schema that would be used for this entity type. /// - /// The entity type to get the table name for. + /// The entity type to get the table schema for. /// The default database schema to which the entity type would be mapped. public static string GetDefaultSchema([NotNull] this IEntityType entityType) { @@ -109,7 +124,7 @@ public static string GetDefaultSchema([NotNull] this IEntityType entityType) } return entityType.HasDefiningNavigation() - ? entityType.DefiningEntityType.GetSchema() + ? entityType.DefiningEntityType.GetSchema() ?? entityType.Model.GetDefaultSchema() : entityType.Model.GetDefaultSchema(); } @@ -174,28 +189,119 @@ public static IEnumerable GetViewMappings([NotNull] this IEntityTy /// /// 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) + public static string GetViewName([NotNull] this IEntityType entityType) => + entityType.BaseType != null + ? entityType.GetRootType().GetViewName() + : (string)entityType[RelationalAnnotationNames.ViewName] + ?? (((entityType as IConventionEntityType)?.GetDefiningQueryConfigurationSource() == null) + ? GetDefaultViewName(entityType) + : null); + + /// + /// Returns the default view name that would be used for this entity type. + /// + /// The entity type to get the table name for. + /// The default name of the table to which the entity type would be mapped. + public static string GetDefaultViewName([NotNull] this IEntityType entityType) { - if (entityType.BaseType != null) - { - return entityType.GetRootType().GetViewName(); - } + var ownership = entityType.FindOwnership(); + return ownership != null + && ownership.IsUnique + ? ownership.PrincipalEntityType.GetViewName() + : null; + } - if (entityType.FindAnnotation(RelationalAnnotationNames.ViewDefinition) != null) - { - return entityType.GetTableName(); - } + /// + /// Sets the name of the view to which the entity type is mapped. + /// + /// The entity type to set the view name for. + /// The name to set. + public static void SetViewName([NotNull] this IMutableEntityType entityType, [CanBeNull] string name) + => entityType.SetAnnotation( + RelationalAnnotationNames.ViewName, + Check.NullButNotEmpty(name, nameof(name))); + + /// + /// Sets the name of the view to which the entity type is mapped. + /// + /// The entity type to set the view name for. + /// The name to set. + /// Indicates whether the configuration was specified using a data annotation. + public static void SetViewName( + [NotNull] this IConventionEntityType entityType, [CanBeNull] string name, bool fromDataAnnotation = false) + => entityType.SetAnnotation( + RelationalAnnotationNames.ViewName, + Check.NullButNotEmpty(name, nameof(name)), + fromDataAnnotation); + + /// + /// Gets the for the view name. + /// + /// The entity type to find configuration source for. + /// The for the view name. + public static ConfigurationSource? GetViewNameConfigurationSource([NotNull] this IConventionEntityType entityType) + => entityType.FindAnnotation(RelationalAnnotationNames.ViewName) + ?.GetConfigurationSource(); + + /// + /// Returns the database schema that contains the mapped view. + /// + /// The entity type to get the view schema for. + /// The database schema that contains the mapped view. + public static string GetViewSchema([NotNull] this IEntityType entityType) => + entityType.BaseType != null + ? entityType.GetRootType().GetViewSchema() + : (string)entityType[RelationalAnnotationNames.ViewSchema] ?? GetDefaultViewSchema(entityType); + /// + /// Returns the default database schema that would be used for this entity view. + /// + /// The entity type to get the view schema for. + /// The default database schema to which the entity type would be mapped. + public static string GetDefaultViewSchema([NotNull] this IEntityType entityType) + { var ownership = entityType.FindOwnership(); if (ownership != null && ownership.IsUnique) { - return ownership.PrincipalEntityType.GetViewName(); + return ownership.PrincipalEntityType.GetViewSchema(); } return null; } + /// + /// Sets the database schema that contains the mapped view. + /// + /// The entity type to set the view schema for. + /// The value to set. + public static void SetViewSchema([NotNull] this IMutableEntityType entityType, [CanBeNull] string value) + => entityType.SetOrRemoveAnnotation( + RelationalAnnotationNames.ViewSchema, + Check.NullButNotEmpty(value, nameof(value))); + + /// + /// Sets the database schema that contains the mapped view. + /// + /// The entity type to set the view schema for. + /// The value to set. + /// Indicates whether the configuration was specified using a data annotation. + public static void SetViewSchema( + [NotNull] this IConventionEntityType entityType, [CanBeNull] string value, bool fromDataAnnotation = false) + => entityType.SetOrRemoveAnnotation( + RelationalAnnotationNames.ViewSchema, + Check.NullButNotEmpty(value, nameof(value)), + fromDataAnnotation); + + /// + /// Gets the for the view schema. + /// + /// The entity type to find configuration source for. + /// The for the view schema. + public static ConfigurationSource? GetViewSchemaConfigurationSource([NotNull] this IConventionEntityType entityType) + => entityType.FindAnnotation(RelationalAnnotationNames.ViewSchema) + ?.GetConfigurationSource(); + /// /// Finds an with the given name. /// diff --git a/src/EFCore.Relational/Extensions/RelationalKeyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalKeyExtensions.cs index 8f50bbfe5c3..2bd43b3a1a2 100644 --- a/src/EFCore.Relational/Extensions/RelationalKeyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalKeyExtensions.cs @@ -32,14 +32,14 @@ public static string GetName([NotNull] this IKey key) => /// The default key constraint name that would be used for this key. public static string GetDefaultName([NotNull] this IKey key) { - var sharedTablePrincipalPrimaryKeyProperty = key.Properties[0].FindSharedTableRootPrimaryKeyProperty(); + var sharedTablePrincipalPrimaryKeyProperty = key.Properties[0].FindSharedRootPrimaryKeyProperty(); if (sharedTablePrincipalPrimaryKeyProperty != null) { return sharedTablePrincipalPrimaryKeyProperty.FindContainingPrimaryKey().GetName(); } var builder = new StringBuilder(); - var tableName = key.DeclaringEntityType.GetTableName(); + var tableName = key.DeclaringEntityType.GetTableName() ?? key.DeclaringEntityType.GetViewName(); if (key.IsPrimaryKey()) { diff --git a/src/EFCore.Relational/Extensions/RelationalPropertyBuilderExtensions.cs b/src/EFCore.Relational/Extensions/RelationalPropertyBuilderExtensions.cs index 2a9890242a3..d4d6e6b3bfe 100644 --- a/src/EFCore.Relational/Extensions/RelationalPropertyBuilderExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalPropertyBuilderExtensions.cs @@ -82,6 +82,73 @@ public static bool CanSetColumnName( bool fromDataAnnotation = false) => propertyBuilder.CanSetAnnotation(RelationalAnnotationNames.ColumnName, name, fromDataAnnotation); + /// + /// Configures the column that the property maps to in a view in a relational database. + /// + /// The builder for the property being configured. + /// The name of the column. + /// The same builder instance so that multiple calls can be chained. + public static PropertyBuilder HasViewColumnName( + [NotNull] this PropertyBuilder propertyBuilder, + [CanBeNull] string name) + { + Check.NotNull(propertyBuilder, nameof(propertyBuilder)); + Check.NullButNotEmpty(name, nameof(name)); + + propertyBuilder.Metadata.SetViewColumnName(name); + + return propertyBuilder; + } + + /// + /// Configures the column that the property maps to in a view in a relational database. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The name of the column. + /// The same builder instance so that multiple calls can be chained. + public static PropertyBuilder HasViewColumnName( + [NotNull] this PropertyBuilder propertyBuilder, + [CanBeNull] string name) + => (PropertyBuilder)HasViewColumnName((PropertyBuilder)propertyBuilder, name); + + /// + /// Configures the column that the property maps to in a view in a relational database. + /// + /// The builder for the property being configured. + /// The name of the column. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// null otherwise. + /// + public static IConventionPropertyBuilder HasViewColumnName( + [NotNull] this IConventionPropertyBuilder propertyBuilder, + [CanBeNull] string name, + bool fromDataAnnotation = false) + { + if (!propertyBuilder.CanSetViewColumnName(name, fromDataAnnotation)) + { + return null; + } + + propertyBuilder.Metadata.SetViewColumnName(name, fromDataAnnotation); + return propertyBuilder; + } + + /// + /// Returns a value indicating whether the given view column can be set for the property. + /// + /// The builder for the property being configured. + /// The name of the column. + /// Indicates whether the configuration was specified using a data annotation. + /// true if the property can be mapped to the given column. + public static bool CanSetViewColumnName( + [NotNull] this IConventionPropertyBuilder propertyBuilder, + [CanBeNull] string name, + bool fromDataAnnotation = false) + => propertyBuilder.CanSetAnnotation(RelationalAnnotationNames.ViewColumnName, name, fromDataAnnotation); + /// /// Configures the data type of the column that the property maps to when targeting a relational database. /// This should be the complete type name, including precision, scale, length, etc. diff --git a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs index b271422cb3f..e74bff3ac93 100644 --- a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs @@ -58,7 +58,9 @@ public static string GetDefaultColumnName([NotNull] this IProperty property) else { var ownerType = ownership.PrincipalEntityType; - if (entityType.GetTableName() == ownerType.GetTableName() + var table = entityType.GetTableName(); + if (table != null + && table == ownerType.GetTableName() && entityType.GetSchema() == ownerType.GetSchema()) { if (builder == null) @@ -81,7 +83,7 @@ public static string GetDefaultColumnName([NotNull] this IProperty property) var baseName = property.Name; if (builder != null) { - builder.Append(property.Name); + builder.Append(baseName); baseName = builder.ToString(); } @@ -119,6 +121,102 @@ public static void SetColumnName( public static ConfigurationSource? GetColumnNameConfigurationSource([NotNull] this IConventionProperty property) => property.FindAnnotation(RelationalAnnotationNames.ColumnName)?.GetConfigurationSource(); + /// + /// Returns the name of the view column to which the property is mapped. + /// + /// The property. + /// The name of the view column to which the property is mapped. + public static string GetViewColumnName([NotNull] this IProperty property) + => (string)property[RelationalAnnotationNames.ViewColumnName] + ?? GetDefaultViewColumnName(property); + + /// + /// Returns the default view column name to which the property would be mapped. + /// + /// The property. + /// The default view column name to which the property would be mapped. + public static string GetDefaultViewColumnName([NotNull] this IProperty property) + { + var sharedTablePrincipalPrimaryKeyProperty = property.FindSharedViewRootPrimaryKeyProperty(); + if (sharedTablePrincipalPrimaryKeyProperty != null) + { + return sharedTablePrincipalPrimaryKeyProperty.GetViewColumnName(); + } + + var entityType = property.DeclaringEntityType; + StringBuilder builder = null; + do + { + var ownership = entityType.GetForeignKeys().SingleOrDefault(fk => fk.IsOwnership); + if (ownership == null) + { + entityType = null; + } + else + { + var ownerType = ownership.PrincipalEntityType; + var viewName = entityType.GetViewName(); + if (viewName != null + && viewName == ownerType.GetViewName() + && entityType.GetViewSchema() == ownerType.GetViewSchema()) + { + if (builder == null) + { + builder = new StringBuilder(); + } + + builder.Insert(0, "_"); + builder.Insert(0, ownership.PrincipalToDependent.Name); + entityType = ownerType; + } + else + { + entityType = null; + } + } + } + while (entityType != null); + + if (builder == null) + { + return property.GetColumnName(); + } + + builder.Append(property.Name); + return Uniquifier.Truncate(builder.ToString(), property.DeclaringEntityType.Model.GetMaxIdentifierLength()); + } + + /// + /// Sets the view column to which the property is mapped. + /// + /// The property. + /// The name to set. + public static void SetViewColumnName([NotNull] this IMutableProperty property, [CanBeNull] string name) + => property.SetOrRemoveAnnotation( + RelationalAnnotationNames.ViewColumnName, + Check.NullButNotEmpty(name, nameof(name))); + + /// + /// Sets the view column to which the property is mapped. + /// + /// The property. + /// The name to set. + /// Indicates whether the configuration was specified using a data annotation. + public static void SetViewColumnName( + [NotNull] this IConventionProperty property, [CanBeNull] string name, bool fromDataAnnotation = false) + => property.SetOrRemoveAnnotation( + RelationalAnnotationNames.ViewColumnName, + Check.NullButNotEmpty(name, nameof(name)), + fromDataAnnotation); + + /// + /// Gets the for the view column name. + /// + /// The property. + /// The for the view column name. + public static ConfigurationSource? GetViewColumnNameConfigurationSource([NotNull] this IConventionProperty property) + => property.FindAnnotation(RelationalAnnotationNames.ViewColumnName)?.GetConfigurationSource(); + /// /// Returns the database type of the column to which the property is mapped. /// @@ -137,7 +235,7 @@ public static string GetColumnType([NotNull] this IProperty property) private static string GetDefaultColumnType(IProperty property) { - var sharedTablePrincipalPrimaryKeyProperty = property.FindSharedTableRootPrimaryKeyProperty(); + var sharedTablePrincipalPrimaryKeyProperty = property.FindSharedRootPrimaryKeyProperty(); return sharedTablePrincipalPrimaryKeyProperty != null ? sharedTablePrincipalPrimaryKeyProperty.GetColumnType() : property.FindRelationalTypeMapping()?.StoreType; @@ -211,7 +309,7 @@ public static string GetDefaultValueSql([NotNull] this IProperty property) return sql; } - var sharedTablePrincipalPrimaryKeyProperty = property.FindSharedTableRootPrimaryKeyProperty(); + var sharedTablePrincipalPrimaryKeyProperty = property.FindSharedRootPrimaryKeyProperty(); if (sharedTablePrincipalPrimaryKeyProperty != null) { return GetDefaultValueSql(sharedTablePrincipalPrimaryKeyProperty); @@ -264,7 +362,7 @@ public static string GetComputedColumnSql([NotNull] this IProperty property) return sql; } - var sharedTablePrincipalPrimaryKeyProperty = property.FindSharedTableRootPrimaryKeyProperty(); + var sharedTablePrincipalPrimaryKeyProperty = property.FindSharedRootPrimaryKeyProperty(); if (sharedTablePrincipalPrimaryKeyProperty != null) { return GetComputedColumnSql(sharedTablePrincipalPrimaryKeyProperty); @@ -317,7 +415,7 @@ public static object GetDefaultValue([NotNull] this IProperty property) return value; } - var sharedTablePrincipalPrimaryKeyProperty = property.FindSharedTableRootPrimaryKeyProperty(); + var sharedTablePrincipalPrimaryKeyProperty = property.FindSharedRootPrimaryKeyProperty(); if (sharedTablePrincipalPrimaryKeyProperty != null) { return GetDefaultValue(sharedTablePrincipalPrimaryKeyProperty); @@ -442,7 +540,7 @@ public static RelationalTypeMapping FindRelationalTypeMapping([NotNull] this IPr /// /// - /// Checks whether or not the column mapped to the given will be nullable + /// Checks whether the column mapped to the given will be nullable /// or not when created in the database. /// /// @@ -456,15 +554,30 @@ public static bool IsColumnNullable([NotNull] this IProperty property) => !property.IsPrimaryKey() && (property.DeclaringEntityType.BaseType != null || property.IsNullable - || IsTableSplitting(property.DeclaringEntityType)); + || property.DeclaringEntityType.FindPrimaryKey()?.Properties[0].FindSharedObjectLink() != null); - private static bool IsTableSplitting(IEntityType entityType) - => entityType.FindPrimaryKey()?.Properties[0].FindSharedTableLink() != null; + /// + /// + /// Checks whether or not the column mapped to the given will be nullable + /// or not when created in the database. + /// + /// + /// This can depend not just on the property itself, but also how it is mapped. For example, + /// non-nullable properties in a TPH type hierarchy will be mapped to nullable columns. + /// + /// + /// The . + /// True if the mapped column is nullable; false otherwise. + public static bool IsViewColumnNullable([NotNull] this IProperty property) + => !property.IsPrimaryKey() + && (property.DeclaringEntityType.BaseType != null + || property.IsNullable + || property.DeclaringEntityType.FindPrimaryKey()?.Properties[0].FindSharedObjectLink(StoreObjectType.View) != null); /// /// - /// Finds the that represents the same primary key property - /// as the given property, but potentially in a shared root table. + /// Finds the that is mapped to the same column as primary key property + /// as the given property, but potentially in a shared table and is not in a shared table foreign key. /// /// /// This type is typically used by database providers (and other extensions). It is generally @@ -474,6 +587,40 @@ private static bool IsTableSplitting(IEntityType entityType) /// The property. /// The property found, or null if none was found. public static IProperty FindSharedTableRootPrimaryKeyProperty([NotNull] this IProperty property) + => FindSharedObjectRootPrimaryKeyProperty(property, StoreObjectType.Table); + + /// + /// + /// Finds the that is mapped to the same database object as primary key property + /// as the given property, but potentially in a shared object and is not in a shared object foreign key. + /// + /// + /// This type is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + /// + /// The property. + /// The property found, or null if none was found. + public static IProperty FindSharedRootPrimaryKeyProperty([NotNull] this IProperty property) + => FindSharedObjectRootPrimaryKeyProperty(property, StoreObjectType.Table) + ?? FindSharedObjectRootPrimaryKeyProperty(property, StoreObjectType.View); + + /// + /// + /// Finds the that is mapped to the same column as primary key property + /// as the given property, but potentially in a shared view and is not in a shared view foreign key. + /// + /// + /// This type is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + /// + /// The property. + /// The property found, or null if none was found. + public static IProperty FindSharedViewRootPrimaryKeyProperty([NotNull] this IProperty property) + => FindSharedObjectRootPrimaryKeyProperty(property, StoreObjectType.View); + + private static IProperty FindSharedObjectRootPrimaryKeyProperty([NotNull] IProperty property, StoreObjectType storeObjectType) { Check.NotNull(property, nameof(property)); @@ -481,7 +628,7 @@ public static IProperty FindSharedTableRootPrimaryKeyProperty([NotNull] this IPr HashSet visitedTypes = null; while (true) { - var linkingRelationship = principalProperty.FindSharedTableLink(); + var linkingRelationship = principalProperty.FindSharedObjectLink(storeObjectType); if (linkingRelationship == null) { break; @@ -516,7 +663,7 @@ public static string GetComment([NotNull] this IProperty property) return value; } - var sharedTablePrincipalPrimaryKeyProperty = property.FindSharedTableRootPrimaryKeyProperty(); + var sharedTablePrincipalPrimaryKeyProperty = property.FindSharedRootPrimaryKeyProperty(); if (sharedTablePrincipalPrimaryKeyProperty != null) { return GetComment(sharedTablePrincipalPrimaryKeyProperty); diff --git a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs index 1ebbec7bed3..f651dc690d9 100644 --- a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs +++ b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs @@ -89,6 +89,7 @@ public override ConventionSet CreateConventionSet() conventionSet.ModelInitializedConventions.Add(dbFunctionAttributeConvention); conventionSet.ModelAnnotationChangedConventions.Add(dbFunctionAttributeConvention); + conventionSet.ModelFinalizingConventions.Add(tableNameFromDbSetConvention); conventionSet.ModelFinalizingConventions.Add(storeGenerationConvention); conventionSet.ModelFinalizingConventions.Add(new SharedTableConvention(Dependencies, RelationalDependencies)); conventionSet.ModelFinalizingConventions.Add(new DbFunctionTypeMappingConvention(Dependencies, RelationalDependencies)); diff --git a/src/EFCore.Relational/Metadata/Conventions/RelationalValueGenerationConvention.cs b/src/EFCore.Relational/Metadata/Conventions/RelationalValueGenerationConvention.cs index 7780af06918..657f34012f2 100644 --- a/src/EFCore.Relational/Metadata/Conventions/RelationalValueGenerationConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/RelationalValueGenerationConvention.cs @@ -96,8 +96,8 @@ private void ProcessTableChanged(IConventionEntityTypeBuilder entityTypeBuilder, return; } - var oldLink = pk.Properties.First().FindSharedTableLink(oldTable, oldSchema); - var newLink = pk.Properties.First().FindSharedTableLink(); + var oldLink = pk.Properties.First().FindSharedObjectLink(oldTable, oldSchema); + var newLink = pk.Properties.First().FindSharedObjectLink(); if (oldLink == null && newLink == null) diff --git a/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs b/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs index 108e30b3d90..8d095b63666 100644 --- a/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs @@ -69,7 +69,8 @@ private static void TryUniquifyTableNames( foreach (var entityType in model.GetEntityTypes()) { var tableName = (Schema: entityType.GetSchema(), TableName: entityType.GetTableName()); - if (tableName.TableName == null) + if (tableName.TableName == null + || entityType.FindPrimaryKey() == null) { continue; } @@ -83,7 +84,6 @@ private static void TryUniquifyTableNames( if (entityTypes.Count > 0) { var shouldUniquifyTable = ShouldUniquify(entityType, entityTypes); - if (shouldUniquifyTable) { if (entityType[RelationalAnnotationNames.TableName] == null) diff --git a/src/EFCore.Relational/Metadata/Conventions/TableNameFromDbSetConvention.cs b/src/EFCore.Relational/Metadata/Conventions/TableNameFromDbSetConvention.cs index 9c895d9e926..9d10a28422a 100644 --- a/src/EFCore.Relational/Metadata/Conventions/TableNameFromDbSetConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/TableNameFromDbSetConvention.cs @@ -13,7 +13,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions /// /// A convention that configures the table name based on the property name. /// - public class TableNameFromDbSetConvention : IEntityTypeAddedConvention, IEntityTypeBaseTypeChangedConvention + public class TableNameFromDbSetConvention : IEntityTypeAddedConvention, IEntityTypeBaseTypeChangedConvention, IModelFinalizingConvention { private readonly IDictionary _sets; @@ -49,22 +49,19 @@ public virtual void ProcessEntityTypeBaseTypeChanged( IConventionEntityType oldBaseType, IConventionContext context) { - if (_sets != null) - { - var entityType = entityTypeBuilder.Metadata; + var entityType = entityTypeBuilder.Metadata; - if (oldBaseType == null - && newBaseType != null) - { - entityTypeBuilder.ToTable(null); - } - else if (oldBaseType != null - && newBaseType == null - && entityType.ClrType != null - && _sets.ContainsKey(entityType.ClrType)) - { - entityTypeBuilder.ToTable(_sets[entityType.ClrType].Name); - } + if (oldBaseType == null + && newBaseType != null) + { + entityTypeBuilder.ToTable(null); + } + else if (oldBaseType != null + && newBaseType == null + && entityType.ClrType != null + && _sets.ContainsKey(entityType.ClrType)) + { + entityTypeBuilder.ToTable(_sets[entityType.ClrType].Name); } } @@ -85,5 +82,20 @@ public virtual void ProcessEntityTypeAdded( entityTypeBuilder.ToTable(_sets[entityType.ClrType].Name); } } + + /// + public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext context) + { + foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) + { + if (entityType.GetTableName() != null + && entityType.GetViewNameConfigurationSource() != null + && _sets.ContainsKey(entityType.ClrType)) + { + // Undo the convention change if the entity type is mapped to a view + entityType.Builder.ToTable(null); + } + } + } } } diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs index 04d3cb2c7ee..484bb012a6b 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs @@ -29,8 +29,7 @@ public static IModel AddRelationalModel([NotNull] IConventionModel model) { var tableName = entityType.GetTableName(); var viewName = entityType.GetViewName(); - if (tableName != null - && viewName == null) + if (tableName != null) { var schema = entityType.GetSchema(); if (!tables.TryGetValue((tableName, schema), out var table)) @@ -86,7 +85,7 @@ public static IModel AddRelationalModel([NotNull] IConventionModel model) if (viewName != null) { - var schema = entityType.GetSchema(); + var schema = entityType.GetViewSchema(); if (!views.TryGetValue((viewName, schema), out var view)) { view = new View(viewName, schema); @@ -97,15 +96,15 @@ public static IModel AddRelationalModel([NotNull] IConventionModel model) foreach (var property in entityType.GetDeclaredProperties()) { var typeMapping = property.FindRelationalTypeMapping(); - var columnName = property.GetColumnName(); + var columnName = property.GetViewColumnName(); var column = (ViewColumn)view.FindColumn(columnName); if (column == null) { column = new ViewColumn(columnName, property.GetColumnType() ?? typeMapping.StoreType, view); - column.IsNullable = property.IsColumnNullable(); + column.IsNullable = property.IsViewColumnNullable(); view.Columns.Add(columnName, column); } - else if (!property.IsColumnNullable()) + else if (!property.IsViewColumnNullable()) { column.IsNullable = false; } diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalPropertyExtensions.cs b/src/EFCore.Relational/Metadata/Internal/RelationalPropertyExtensions.cs index 0ca625b3def..1a37f1337d1 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalPropertyExtensions.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalPropertyExtensions.cs @@ -1,6 +1,7 @@ // 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.Diagnostics; using JetBrains.Annotations; @@ -20,8 +21,26 @@ public static class RelationalPropertyExtensions /// any release. You should only use it directly in your 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 IForeignKey FindSharedTableLink([NotNull] this IProperty property) - => property.FindSharedTableLink(property.DeclaringEntityType.GetTableName(), property.DeclaringEntityType.GetSchema()); + public static IForeignKey FindSharedObjectLink( + [NotNull] this IProperty property, + StoreObjectType objectType = StoreObjectType.Table) + { + switch (objectType) + { + case StoreObjectType.Table: + return property.FindSharedObjectLink( + property.DeclaringEntityType.GetTableName(), + property.DeclaringEntityType.GetSchema(), + objectType); + case StoreObjectType.View: + return property.FindSharedObjectLink( + property.DeclaringEntityType.GetViewName(), + property.DeclaringEntityType.GetViewSchema(), + objectType); + default: + throw new NotImplementedException(objectType.ToString()); + } + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -29,10 +48,15 @@ public static IForeignKey FindSharedTableLink([NotNull] this IProperty property) /// any release. You should only use it directly in your 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 IForeignKey FindSharedTableLink([NotNull] this IProperty property, [CanBeNull] string table, [CanBeNull] string schema) + public static IForeignKey FindSharedObjectLink( + [NotNull] this IProperty property, + [CanBeNull] string name, + [CanBeNull] string schema, + StoreObjectType objectType = StoreObjectType.Table) { var pk = property.FindContainingPrimaryKey(); - if (pk == null) + if (pk == null + || name == null) { return null; } @@ -48,10 +72,24 @@ public static IForeignKey FindSharedTableLink([NotNull] this IProperty property, } var principalEntityType = fk.PrincipalEntityType; - if (table == principalEntityType.GetTableName() - && schema == principalEntityType.GetSchema()) + switch (objectType) { - return fk; + case StoreObjectType.Table: + if (name == principalEntityType.GetTableName() + && schema == principalEntityType.GetSchema()) + { + return fk; + } + break; + case StoreObjectType.View: + if (name == principalEntityType.GetViewName() + && schema == principalEntityType.GetViewSchema()) + { + return fk; + } + break; + default: + throw new NotImplementedException(objectType.ToString()); } } diff --git a/src/EFCore.Relational/Metadata/Internal/StoreObjectType.cs b/src/EFCore.Relational/Metadata/Internal/StoreObjectType.cs new file mode 100644 index 00000000000..5b57f8da4fa --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/StoreObjectType.cs @@ -0,0 +1,38 @@ +// 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.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 enum StoreObjectType + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + 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. + /// + View, + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + Function + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/ViewColumnMappingComparer.cs b/src/EFCore.Relational/Metadata/Internal/ViewColumnMappingComparer.cs index ec1bf6ea2fc..ab7fba38ad1 100644 --- a/src/EFCore.Relational/Metadata/Internal/ViewColumnMappingComparer.cs +++ b/src/EFCore.Relational/Metadata/Internal/ViewColumnMappingComparer.cs @@ -34,7 +34,7 @@ private ViewColumnMappingComparer() /// public virtual int Compare(IViewColumnMapping x, IViewColumnMapping y) { - var result = StringComparer.Ordinal.Compare(x.Property.IsColumnNullable(), y.Property.IsColumnNullable()); + var result = StringComparer.Ordinal.Compare(x.Property.IsViewColumnNullable(), y.Property.IsViewColumnNullable()); if (result != 0) { return result; diff --git a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs index a941404e648..f6fc31415fb 100644 --- a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs +++ b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs @@ -19,6 +19,11 @@ public static class RelationalAnnotationNames /// public const string ColumnName = Prefix + "ColumnName"; + /// + /// The name for column name annotations. + /// + public const string ViewColumnName = Prefix + "ViewColumnName"; + /// /// The name for column type annotations. /// @@ -49,6 +54,16 @@ public static class RelationalAnnotationNames /// public const string Schema = Prefix + "Schema"; + /// + /// The name for view name annotations. + /// + public const string ViewName = Prefix + "ViewName"; + + /// + /// The name for view schema name annotations. + /// + public const string ViewSchema = Prefix + "ViewSchema"; + /// /// The name for comment annotations. /// diff --git a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs index 08c1d8b3b8c..0a6a04fcfff 100644 --- a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs +++ b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs @@ -864,8 +864,7 @@ protected virtual IEnumerable Diff( (s, t, c) => PropertyStructureEquals(s, t)); private bool PropertyStructureEquals(IProperty source, IProperty target) - => - source.ClrType == target.ClrType + => source.ClrType == target.ClrType && source.IsConcurrencyToken == target.IsConcurrencyToken && source.ValueGenerated == target.ValueGenerated && source.GetMaxLength() == target.GetMaxLength() diff --git a/src/EFCore.SqlServer/Extensions/SqlServerKeyExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerKeyExtensions.cs index af77effe13e..b78be0cbdaf 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerKeyExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerKeyExtensions.cs @@ -23,7 +23,7 @@ public static class SqlServerKeyExtensions private static bool? GetDefaultIsClustered(IKey key) { - var sharedTablePrincipalPrimaryKeyProperty = key.Properties[0].FindSharedTableRootPrimaryKeyProperty(); + var sharedTablePrincipalPrimaryKeyProperty = key.Properties[0].FindSharedRootPrimaryKeyProperty(); return sharedTablePrincipalPrimaryKeyProperty?.FindContainingPrimaryKey().IsClustered(); } diff --git a/src/EFCore.SqlServer/Extensions/SqlServerPropertyExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerPropertyExtensions.cs index 07a5b036fa1..1623e19db96 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerPropertyExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerPropertyExtensions.cs @@ -212,7 +212,7 @@ public static SqlServerValueGenerationStrategy GetValueGenerationStrategy([NotNu return (SqlServerValueGenerationStrategy)annotation; } - var sharedTablePrincipalPrimaryKeyProperty = property.FindSharedTableRootPrimaryKeyProperty(); + var sharedTablePrincipalPrimaryKeyProperty = property.FindSharedRootPrimaryKeyProperty(); if (sharedTablePrincipalPrimaryKeyProperty != null) { return sharedTablePrincipalPrimaryKeyProperty.GetValueGenerationStrategy() diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs index 23723b110b0..30081cdea26 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs @@ -330,8 +330,10 @@ public partial class Vista model => { var entityType = model.FindEntityType("TestNamespace.Vista"); - Assert.Equal("Vistas", entityType.GetTableName()); - Assert.Equal("dbo", entityType.GetSchema()); + Assert.Equal("Vistas", entityType.GetViewName()); + Assert.Null(entityType.GetTableName()); + Assert.Equal("dbo", entityType.GetViewSchema()); + Assert.Null(entityType.GetSchema()); }); } diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/RelationalScaffoldingModelFactoryTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/RelationalScaffoldingModelFactoryTest.cs index f0f6a60f099..973331f5309 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/RelationalScaffoldingModelFactoryTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/RelationalScaffoldingModelFactoryTest.cs @@ -91,7 +91,8 @@ public void Creates_entity_types() }, view => { - Assert.Equal("view", view.GetTableName()); + Assert.Equal("view", view.GetViewName()); + Assert.Null(view.GetTableName()); Assert.NotNull(view.FindAnnotation(RelationalAnnotationNames.ViewDefinition)); } ); diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs index 6b1407cfa75..5c49a782a8f 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs @@ -94,7 +94,7 @@ public void Can_get_and_set_table_name() entityType.SetTableName(null); - Assert.Equal("Customer", entityType.GetTableName()); + Assert.Null(entityType.GetTableName()); } [ConditionalFact] @@ -117,6 +117,28 @@ public void Can_get_and_set_schema_name_on_entity_type() Assert.Null(entityType.GetSchema()); } + [ConditionalFact] + public void Can_get_table_and_schema_name_for_non_owned_entity_types_with_defining_navigation() + { + var modelBuilder = new ModelBuilder(new ConventionSet()); + + var orderType = modelBuilder + .Entity() + .Metadata; + + var customerType = modelBuilder.Model.AddEntityType(typeof(Customer), nameof(Order.Customer), orderType); + + Assert.Equal("Order_Customer", customerType.GetTableName()); + + orderType.SetTableName(null); + + Assert.Equal("Customer_Customer", customerType.GetTableName()); + + customerType.SetTableName("Customizer"); + + Assert.Equal("Customizer", customerType.GetTableName()); + } + [ConditionalFact] public void Gets_model_schema_if_schema_on_entity_type_not_set() { @@ -589,6 +611,7 @@ private class Order { public int OrderId { get; set; } public int CustomerId { get; set; } + public Customer Customer { get; set; } } } } diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs index 861a7a2307d..ae73576caff 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs @@ -13,97 +13,123 @@ namespace Microsoft.EntityFrameworkCore.Metadata { public class RelationalModelTest { - [ConditionalFact] - public void Can_use_relational_model_with_tables() + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public void Can_use_relational_model_with_tables(bool useExplicitMapping) { - var model = CreateTestModel(); + var model = CreateTestModel(mapToTables: useExplicitMapping); Assert.Equal(6, model.GetEntityTypes().Count()); Assert.Equal(2, model.GetTables().Count()); Assert.Empty(model.GetViews()); + Assert.True(model.GetEntityTypes().All(et => et.GetViewMappings() == null)); + + AssertTables(model); + } + + [ConditionalFact] + public void Can_use_relational_model_with_views() + { + var model = CreateTestModel(mapToTables: false, mapToViews: true); + + Assert.Equal(6, model.GetEntityTypes().Count()); + Assert.Equal(2, model.GetViews().Count()); + Assert.Empty(model.GetTables()); + Assert.True(model.GetEntityTypes().All(et => et.GetTableMappings() == null)); + + AssertViews(model); + } + + [ConditionalFact] + public void Can_use_relational_model_with_views_and_tables() + { + var model = CreateTestModel(mapToTables: true, mapToViews: true); + + Assert.Equal(6, model.GetEntityTypes().Count()); + Assert.Equal(2, model.GetTables().Count()); + Assert.Equal(2, model.GetViews().Count()); + + AssertTables(model); + AssertViews(model); + } + private static void AssertViews(IModel model) + { var orderType = model.FindEntityType(typeof(Order)); - var orderMapping = orderType.GetTableMappings().Single(); + var orderMapping = orderType.GetViewMappings().Single(); + 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 ordersTable = orderMapping.Table; - Assert.Same(ordersTable, model.FindTable(ordersTable.Name, ordersTable.Schema)); + 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) }, - ordersTable.EntityTypeMappings.Select(m => m.EntityType.DisplayName())); + 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), + "OrderDateView", 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); + ordersView.Columns.Select(m => m.Name)); + Assert.Equal("OrderView", ordersView.Name); + Assert.Equal("viewSchema", ordersView.Schema); + Assert.Null(ordersView.ViewDefinition); var orderDate = orderType.FindProperty(nameof(Order.OrderDate)); - Assert.False(orderDate.IsColumnNullable()); + Assert.False(orderDate.IsViewColumnNullable()); - var orderDateMapping = orderDate.GetTableColumnMappings().Single(); + var orderDateMapping = orderDate.GetViewColumnMappings().Single(); Assert.NotNull(orderDateMapping.TypeMapping); Assert.Equal("default_datetime_mapping", orderDateMapping.TypeMapping.StoreType); - Assert.Same(orderMapping, orderDateMapping.TableMapping); + Assert.Same(orderMapping, orderDateMapping.ViewMapping); var orderDetailsOwnership = orderType.FindNavigation(nameof(Order.Details)).ForeignKey; var orderDetailsType = orderDetailsOwnership.DeclaringEntityType; - Assert.Same(ordersTable, orderDetailsType.GetTableMappings().Single().Table); - Assert.Equal(ordersTable.GetReferencingInternalForeignKeys(orderType), ordersTable.GetInternalForeignKeys(orderDetailsType)); + 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()); + Assert.True(orderDetailsDate.IsViewColumnNullable()); var orderDateColumn = orderDateMapping.Column; - Assert.Same(orderDateColumn, ordersTable.FindColumn("OrderDate")); + Assert.Same(orderDateColumn, ordersView.FindColumn("OrderDateView")); Assert.Equal(new[] { orderDate, orderDetailsDate }, orderDateColumn.PropertyMappings.Select(m => m.Property)); - Assert.Equal("OrderDate", orderDateColumn.Name); + Assert.Equal("OrderDateView", orderDateColumn.Name); Assert.Equal("default_datetime_mapping", orderDateColumn.Type); Assert.False(orderDateColumn.IsNullable); - Assert.Same(ordersTable, orderDateColumn.Table); + Assert.Same(ordersView, orderDateColumn.Table); var customerType = model.FindEntityType(typeof(Customer)); - var customerTable = customerType.GetTableMappings().Single().Table; - Assert.Equal("Customer", customerTable.Name); + var customerView = customerType.GetViewMappings().Single().Table; + Assert.Equal("CustomerView", customerView.Name); + Assert.Equal("viewSchema", customerView.Schema); var specialCustomerType = model.FindEntityType(typeof(SpecialCustomer)); - Assert.Same(customerTable, specialCustomerType.GetTableMappings().Single().Table); + Assert.Same(customerView, specialCustomerType.GetViewMappings().Single().Table); } - [ConditionalFact] - public void Can_use_relational_model_with_views() + private static void AssertTables(IModel model) { - 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()); + 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 ordersView = orderMapping.View; - Assert.Same(ordersView, model.FindView(ordersView.Name, ordersView.Schema)); + 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) }, - ordersView.EntityTypeMappings.Select(m => m.EntityType.DisplayName())); + ordersTable.EntityTypeMappings.Select(m => m.EntityType.DisplayName())); Assert.Equal(new[] { nameof(Order.CustomerId), "Details_BillingAddress_City", @@ -113,44 +139,45 @@ public void Can_use_relational_model_with_views() 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); + 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.GetViewColumnMappings().Single(); + var orderDateMapping = orderDate.GetTableColumnMappings().Single(); Assert.NotNull(orderDateMapping.TypeMapping); Assert.Equal("default_datetime_mapping", orderDateMapping.TypeMapping.StoreType); - Assert.Same(orderMapping, orderDateMapping.ViewMapping); + Assert.Same(orderMapping, orderDateMapping.TableMapping); 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)); + Assert.Same(ordersTable, orderDetailsType.GetTableMappings().Single().Table); + 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, ordersView.FindColumn("OrderDate")); + 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(ordersView, orderDateColumn.Table); + Assert.Same(ordersTable, orderDateColumn.Table); var customerType = model.FindEntityType(typeof(Customer)); - var customerView = customerType.GetViewMappings().Single().Table; - Assert.Equal("CustomerView", customerView.Name); + var customerTable = customerType.GetTableMappings().Single().Table; + Assert.Equal("Customer", customerTable.Name); var specialCustomerType = model.FindEntityType(typeof(SpecialCustomer)); - Assert.Same(customerView, specialCustomerType.GetViewMappings().Single().Table); + Assert.Same(customerTable, specialCustomerType.GetTableMappings().Single().Table); } - private IModel CreateTestModel(bool mapToViews = false) + private IModel CreateTestModel(bool mapToTables = false, bool mapToViews = false) { var modelBuilder = CreateConventionModelBuilder(); @@ -158,23 +185,36 @@ private IModel CreateTestModel(bool mapToViews = false) { if (mapToViews) { - cb.ToView("CustomerView"); + cb.ToView("CustomerView", "viewSchema"); + } + + if (mapToTables) + { + cb.ToTable("Customer"); } }); modelBuilder.Entity(); modelBuilder.Entity(ob => { ob.Property(od => od.OrderDate).HasColumnName("OrderDate"); + ob.Property(od => od.OrderDate).HasViewColumnName("OrderDateView"); + ob.OwnsOne(o => o.Details, odb => { odb.Property(od => od.OrderDate).HasColumnName("OrderDate"); + odb.Property(od => od.OrderDate).HasViewColumnName("OrderDateView"); + odb.OwnsOne(od => od.BillingAddress); odb.OwnsOne(od => od.ShippingAddress); }); if (mapToViews) { - ob.ToView("OrderView"); + ob.ToView("OrderView", "viewSchema"); + } + if (mapToTables) + { + ob.ToTable("Order"); } });