From ab2ea8f82d1eb530d8dab30676d77289ef4bd6da Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Mon, 20 Jul 2020 17:22:16 -0700 Subject: [PATCH] Validate shared views Fixes #20950 --- .../RelationalForeignKeyExtensions.cs | 88 +++---- .../Extensions/RelationalIndexExtensions.cs | 82 +++---- .../Extensions/RelationalKeyExtensions.cs | 73 ++---- .../RelationalModelValidator.cs | 230 ++++++++++++++---- .../Conventions/SharedTableConvention.cs | 62 ++--- .../RelationalForeignKeyExtensions.cs | 29 ++- .../Internal/RelationalIndexExtensions.cs | 8 +- .../Internal/RelationalKeyExtensions.cs | 6 +- .../Metadata/Internal/RelationalModel.cs | 10 +- .../Metadata/StoreObjectIdentifier.cs | 19 +- .../Properties/RelationalStrings.Designer.cs | 17 +- .../Properties/RelationalStrings.resx | 6 + .../Extensions/SqlServerIndexExtensions.cs | 19 +- .../Extensions/SqlServerKeyExtensions.cs | 19 +- .../Internal/SqlServerModelValidator.cs | 14 +- .../SqlServerSharedTableConvention.cs | 12 +- .../Internal/SqlServerIndexExtensions.cs | 25 +- .../Internal/SqlServerKeyExtensions.cs | 11 +- .../RelationalModelValidatorTest.cs | 78 ++++++ 19 files changed, 462 insertions(+), 346 deletions(-) diff --git a/src/EFCore.Relational/Extensions/RelationalForeignKeyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalForeignKeyExtensions.cs index 4c5eb4d598d..453bde9b57e 100644 --- a/src/EFCore.Relational/Extensions/RelationalForeignKeyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalForeignKeyExtensions.cs @@ -7,7 +7,6 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Utilities; // ReSharper disable once CheckNamespace @@ -25,29 +24,23 @@ public static class RelationalForeignKeyExtensions /// The foreign key constraint name. public static string GetConstraintName([NotNull] this IForeignKey foreignKey) => foreignKey.GetConstraintName( - foreignKey.DeclaringEntityType.GetTableName(), foreignKey.DeclaringEntityType.GetSchema(), - foreignKey.PrincipalEntityType.GetTableName(), foreignKey.PrincipalEntityType.GetSchema()); + StoreObjectIdentifier.Table(foreignKey.DeclaringEntityType.GetTableName(), foreignKey.DeclaringEntityType.GetSchema()), + StoreObjectIdentifier.Table(foreignKey.PrincipalEntityType.GetTableName(), foreignKey.PrincipalEntityType.GetSchema())); /// /// Returns the foreign key constraint name. /// /// The foreign key. - /// The table name. - /// The schema. - /// The principal table name. - /// The principal schema. + /// The identifier of the containing store object. + /// The identifier of the principal store object. /// The foreign key constraint name. public static string GetConstraintName( - [NotNull] this IForeignKey foreignKey, - [NotNull] string tableName, - [CanBeNull] string schema, - [NotNull] string principalTableName, - [CanBeNull] string principalSchema) + [NotNull] this IForeignKey foreignKey, StoreObjectIdentifier storeObject, StoreObjectIdentifier principalStoreObject) { var annotation = foreignKey.FindAnnotation(RelationalAnnotationNames.Name); return annotation != null ? (string)annotation.Value - : foreignKey.GetDefaultName(tableName, schema, principalTableName, principalSchema); + : foreignKey.GetDefaultName(storeObject, principalStoreObject); } /// @@ -77,19 +70,14 @@ public static string GetDefaultName([NotNull] this IForeignKey foreignKey) /// Returns the default constraint name that would be used for this foreign key. /// /// The foreign key. - /// The table name. - /// The schema. - /// The principal table name. - /// The principal schema. + /// The identifier of the containing store object. + /// The identifier of the principal store object. /// The default constraint name that would be used for this foreign key. public static string GetDefaultName( [NotNull] this IForeignKey foreignKey, - [NotNull] string tableName, - [CanBeNull] string schema, - [NotNull] string principalTableName, - [CanBeNull] string principalSchema) + StoreObjectIdentifier storeObject, + StoreObjectIdentifier principalStoreObject) { - var storeObject = StoreObjectIdentifier.Table(tableName, schema); var propertyNames = foreignKey.Properties.Select(p => p.GetColumnName(storeObject)).ToList(); var principalPropertyNames = foreignKey.PrincipalKey.Properties.Select(p => p.GetColumnName(storeObject)).ToList(); var rootForeignKey = foreignKey; @@ -101,8 +89,8 @@ public static string GetDefaultName( var linkedForeignKey = rootForeignKey.DeclaringEntityType .FindRowInternalForeignKeys(storeObject) .SelectMany(fk => fk.PrincipalEntityType.GetForeignKeys()) - .FirstOrDefault(k => principalTableName == k.PrincipalEntityType.GetTableName() - && principalSchema == k.PrincipalEntityType.GetSchema() + .FirstOrDefault(k => principalStoreObject.Name == k.PrincipalEntityType.GetTableName() + && principalStoreObject.Schema == k.PrincipalEntityType.GetSchema() && propertyNames.SequenceEqual(k.Properties.Select(p => p.GetColumnName(storeObject))) && principalPropertyNames.SequenceEqual(k.PrincipalKey.Properties.Select(p => p.GetColumnName(storeObject)))); if (linkedForeignKey == null) @@ -115,14 +103,14 @@ public static string GetDefaultName( if (rootForeignKey != foreignKey) { - return rootForeignKey.GetConstraintName(tableName, schema, principalTableName, principalSchema); + return rootForeignKey.GetConstraintName(storeObject, principalStoreObject); } var baseName = new StringBuilder() .Append("FK_") - .Append(tableName) + .Append(storeObject.Name) .Append("_") - .Append(principalTableName) + .Append(principalStoreObject.Name) .Append("_") .AppendJoin(foreignKey.Properties.Select(p => p.GetColumnName(storeObject)), "_") .ToString(); @@ -178,7 +166,7 @@ public static IEnumerable GetMappedConstraints([NotNull] /// /// - /// Finds the first that is mapped to the same constraint in a shared table. + /// Finds the first that is mapped to the same constraint in a shared table-like object. /// /// /// This method is typically used by database providers (and other extensions). It is generally @@ -186,22 +174,16 @@ public static IEnumerable GetMappedConstraints([NotNull] /// /// /// The foreign key. - /// The table name. - /// The schema. + /// The identifier of the containing store object. /// The foreign key if found, or if none was found. - public static IForeignKey FindSharedTableRootForeignKey( - [NotNull] this IForeignKey foreignKey, - [NotNull] string tableName, - [CanBeNull] string schema) + public static IForeignKey FindSharedObjectRootForeignKey([NotNull] this IForeignKey foreignKey, StoreObjectIdentifier storeObject) { Check.NotNull(foreignKey, nameof(foreignKey)); - Check.NotNull(tableName, nameof(tableName)); - var foreignKeyName = foreignKey.GetConstraintName(tableName, schema, - foreignKey.PrincipalEntityType.GetTableName(), foreignKey.PrincipalEntityType.GetSchema()); + var foreignKeyName = foreignKey.GetConstraintName(storeObject, + StoreObjectIdentifier.Table(foreignKey.PrincipalEntityType.GetTableName(), foreignKey.PrincipalEntityType.GetSchema())); var rootForeignKey = foreignKey; - var storeObject = StoreObjectIdentifier.Table(tableName, schema); // Limit traversal to avoid getting stuck in a cycle (validation will throw for these later) // Using a hashset is detrimental to the perf when there are no cycles for (var i = 0; i < Metadata.Internal.RelationalEntityTypeExtensions.MaxEntityTypesSharingTable; i++) @@ -209,8 +191,8 @@ public static IForeignKey FindSharedTableRootForeignKey( var linkedKey = rootForeignKey.DeclaringEntityType .FindRowInternalForeignKeys(storeObject) .SelectMany(fk => fk.PrincipalEntityType.GetForeignKeys()) - .FirstOrDefault(k => k.GetConstraintName(tableName, schema, - k.PrincipalEntityType.GetTableName(), k.PrincipalEntityType.GetSchema()) + .FirstOrDefault(k => k.GetConstraintName(storeObject, + StoreObjectIdentifier.Table(k.PrincipalEntityType.GetTableName(), k.PrincipalEntityType.GetSchema())) == foreignKeyName); if (linkedKey == null) { @@ -225,7 +207,7 @@ public static IForeignKey FindSharedTableRootForeignKey( /// /// - /// Finds the first that is mapped to the same constraint in a shared table. + /// Finds the first that is mapped to the same constraint in a shared table-like object. /// /// /// This method is typically used by database providers (and other extensions). It is generally @@ -233,18 +215,15 @@ public static IForeignKey FindSharedTableRootForeignKey( /// /// /// The foreign key. - /// The table name. - /// The schema. + /// The identifier of the containing store object. /// The foreign key if found, or if none was found. - public static IMutableForeignKey FindSharedTableRootForeignKey( - [NotNull] this IMutableForeignKey foreignKey, - [NotNull] string tableName, - [CanBeNull] string schema) - => (IMutableForeignKey)((IForeignKey)foreignKey).FindSharedTableRootForeignKey(tableName, schema); + public static IMutableForeignKey FindSharedObjectRootForeignKey( + [NotNull] this IMutableForeignKey foreignKey, StoreObjectIdentifier storeObject) + => (IMutableForeignKey)((IForeignKey)foreignKey).FindSharedObjectRootForeignKey(storeObject); /// /// - /// Finds the first that is mapped to the same constraint in a shared table. + /// Finds the first that is mapped to the same constraint in a shared table-like object. /// /// /// This method is typically used by database providers (and other extensions). It is generally @@ -252,13 +231,10 @@ public static IMutableForeignKey FindSharedTableRootForeignKey( /// /// /// The foreign key. - /// The table name. - /// The schema. + /// The identifier of the containing store object. /// The foreign key if found, or if none was found. - public static IConventionForeignKey FindSharedTableRootForeignKey( - [NotNull] this IConventionForeignKey foreignKey, - [NotNull] string tableName, - [CanBeNull] string schema) - => (IConventionForeignKey)((IForeignKey)foreignKey).FindSharedTableRootForeignKey(tableName, schema); + public static IConventionForeignKey FindSharedObjectRootForeignKey( + [NotNull] this IConventionForeignKey foreignKey, StoreObjectIdentifier storeObject) + => (IConventionForeignKey)((IForeignKey)foreignKey).FindSharedObjectRootForeignKey(storeObject); } } diff --git a/src/EFCore.Relational/Extensions/RelationalIndexExtensions.cs b/src/EFCore.Relational/Extensions/RelationalIndexExtensions.cs index f2c600070b7..5b5c3ae3973 100644 --- a/src/EFCore.Relational/Extensions/RelationalIndexExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalIndexExtensions.cs @@ -41,16 +41,12 @@ public static string GetName([NotNull] this IIndex index) /// Returns the name of the index in the database. /// /// The index. - /// The table name. - /// The schema. + /// The identifier of the store object. /// The name of the index in the database. - public static string GetDatabaseName( - [NotNull] this IIndex index, - [NotNull] string tableName, - [CanBeNull] string schema) + public static string GetDatabaseName([NotNull] this IIndex index, StoreObjectIdentifier storeObject) => (string)index[RelationalAnnotationNames.Name] ?? index.Name - ?? index.GetDefaultDatabaseName(tableName, schema); + ?? index.GetDefaultDatabaseName(storeObject); /// /// Returns the default name that would be used for this index. @@ -84,16 +80,11 @@ public static string GetDefaultName([NotNull] this IIndex index) /// Returns the default name that would be used for this index. /// /// The index. - /// The table name. - /// The schema. + /// The identifier of the store object. /// The default name that would be used for this index. - public static string GetDefaultDatabaseName( - [NotNull] this IIndex index, - [NotNull] string tableName, - [CanBeNull] string schema) + public static string GetDefaultDatabaseName([NotNull] this IIndex index, StoreObjectIdentifier storeObject) { - var table = StoreObjectIdentifier.Table(tableName, schema); - var propertyNames = index.Properties.Select(p => p.GetColumnName(table)).ToList(); + var propertyNames = index.Properties.Select(p => p.GetColumnName(storeObject)).ToList(); var rootIndex = index; // Limit traversal to avoid getting stuck in a cycle (validation will throw for these later) @@ -101,9 +92,9 @@ public static string GetDefaultDatabaseName( for (var i = 0; i < Metadata.Internal.RelationalEntityTypeExtensions.MaxEntityTypesSharingTable; i++) { var linkedIndex = rootIndex.DeclaringEntityType - .FindRowInternalForeignKeys(table) + .FindRowInternalForeignKeys(storeObject) .SelectMany(fk => fk.PrincipalEntityType.GetIndexes()) - .FirstOrDefault(i => i.Properties.Select(p => p.GetColumnName(table)).SequenceEqual(propertyNames)); + .FirstOrDefault(i => i.Properties.Select(p => p.GetColumnName(storeObject)).SequenceEqual(propertyNames)); if (linkedIndex == null) { break; @@ -114,12 +105,12 @@ public static string GetDefaultDatabaseName( if (rootIndex != index) { - return rootIndex.GetDatabaseName(tableName, schema); + return rootIndex.GetDatabaseName(storeObject); } var baseName = new StringBuilder() .Append("IX_") - .Append(tableName) + .Append(storeObject.Name) .Append("_") .AppendJoin(propertyNames, "_") .ToString(); @@ -208,10 +199,9 @@ public static string GetFilter([NotNull] this IIndex index) /// Returns the index filter expression. /// /// The index. - /// The table name. - /// The schema. + /// The identifier of the containing store object. /// The index filter expression. - public static string GetFilter([NotNull] this IIndex index, [NotNull] string tableName, [CanBeNull] string schema) + public static string GetFilter([NotNull] this IIndex index, StoreObjectIdentifier storeObject) { var annotation = index.FindAnnotation(RelationalAnnotationNames.Filter); if (annotation != null) @@ -219,8 +209,8 @@ public static string GetFilter([NotNull] this IIndex index, [NotNull] string tab return (string)annotation.Value; } - var sharedTableRootIndex = index.FindSharedTableRootIndex(tableName, schema); - return sharedTableRootIndex?.GetFilter(tableName, schema); + var sharedTableRootIndex = index.FindSharedObjectRootIndex(storeObject); + return sharedTableRootIndex?.GetFilter(storeObject); } /// @@ -269,7 +259,7 @@ public static IEnumerable GetMappedTableIndexes([NotNull] this IInd /// /// - /// Finds the first that is mapped to the same index in a shared table. + /// Finds the first that is mapped to the same index in a shared table-like object. /// /// /// This method is typically used by database providers (and other extensions). It is generally @@ -277,19 +267,13 @@ public static IEnumerable GetMappedTableIndexes([NotNull] this IInd /// /// /// The index. - /// The table name. - /// The schema. + /// The identifier of the containing store object. /// The index found, or if none was found. - public static IIndex FindSharedTableRootIndex( - [NotNull] this IIndex index, - [NotNull] string tableName, - [CanBeNull] string schema) + public static IIndex FindSharedObjectRootIndex([NotNull] this IIndex index, StoreObjectIdentifier storeObject) { Check.NotNull(index, nameof(index)); - Check.NotNull(tableName, nameof(tableName)); - var table = StoreObjectIdentifier.Table(tableName, schema); - var indexName = index.GetDatabaseName(tableName, schema); + var indexName = index.GetDatabaseName(storeObject); var rootIndex = index; // Limit traversal to avoid getting stuck in a cycle (validation will throw for these later) @@ -297,9 +281,9 @@ public static IIndex FindSharedTableRootIndex( for (var i = 0; i < Metadata.Internal.RelationalEntityTypeExtensions.MaxEntityTypesSharingTable; i++) { var linkedIndex = rootIndex.DeclaringEntityType - .FindRowInternalForeignKeys(table) + .FindRowInternalForeignKeys(storeObject) .SelectMany(fk => fk.PrincipalEntityType.GetIndexes()) - .FirstOrDefault(i => i.GetDatabaseName(tableName, schema) == indexName); + .FirstOrDefault(i => i.GetDatabaseName(storeObject) == indexName); if (linkedIndex == null) { break; @@ -313,7 +297,7 @@ public static IIndex FindSharedTableRootIndex( /// /// - /// Finds the first that is mapped to the same index in a shared table. + /// Finds the first that is mapped to the same index in a shared table-like object. /// /// /// This method is typically used by database providers (and other extensions). It is generally @@ -321,18 +305,15 @@ public static IIndex FindSharedTableRootIndex( /// /// /// The index. - /// The table name. - /// The schema. + /// The identifier of the containing store object. /// The index found, or if none was found. - public static IMutableIndex FindSharedTableRootIndex( - [NotNull] this IMutableIndex index, - [NotNull] string tableName, - [CanBeNull] string schema) - => (IMutableIndex)((IIndex)index).FindSharedTableRootIndex(tableName, schema); + public static IMutableIndex FindSharedObjectRootIndex( + [NotNull] this IMutableIndex index, StoreObjectIdentifier storeObject) + => (IMutableIndex)((IIndex)index).FindSharedObjectRootIndex(storeObject); /// /// - /// Finds the first that is mapped to the same index in a shared table. + /// Finds the first that is mapped to the same index in a shared table-like object. /// /// /// This method is typically used by database providers (and other extensions). It is generally @@ -340,13 +321,10 @@ public static IMutableIndex FindSharedTableRootIndex( /// /// /// The index. - /// The table name. - /// The schema. + /// The identifier of the containing store object. /// The index found, or if none was found. - public static IConventionIndex FindSharedTableRootIndex( - [NotNull] this IConventionIndex index, - [NotNull] string tableName, - [CanBeNull] string schema) - => (IConventionIndex)((IIndex)index).FindSharedTableRootIndex(tableName, schema); + public static IConventionIndex FindSharedObjectRootIndex( + [NotNull] this IConventionIndex index, StoreObjectIdentifier storeObject) + => (IConventionIndex)((IIndex)index).FindSharedObjectRootIndex(storeObject); } } diff --git a/src/EFCore.Relational/Extensions/RelationalKeyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalKeyExtensions.cs index 5b22c61b8fa..c7813ff408f 100644 --- a/src/EFCore.Relational/Extensions/RelationalKeyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalKeyExtensions.cs @@ -7,7 +7,6 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Utilities; // ReSharper disable once CheckNamespace @@ -24,21 +23,17 @@ public static class RelationalKeyExtensions /// The key. /// The key constraint name for this key. public static string GetName([NotNull] this IKey key) - => key.GetName(key.DeclaringEntityType.GetTableName(), key.DeclaringEntityType.GetSchema()); + => key.GetName(StoreObjectIdentifier.Table(key.DeclaringEntityType.GetTableName(), key.DeclaringEntityType.GetSchema())); /// /// Returns the key constraint name for this key for a particular table. /// /// The key. - /// The table name. - /// The schema. + /// The identifier of the containing store object. /// The key constraint name for this key. - public static string GetName( - [NotNull] this IKey key, - [NotNull] string tableName, - [CanBeNull] string schema) + public static string GetName([NotNull] this IKey key, StoreObjectIdentifier storeObject) => (string)key[RelationalAnnotationNames.Name] - ?? key.GetDefaultName(tableName, schema); + ?? key.GetDefaultName(storeObject); /// /// Returns the default key constraint name that would be used for this key. @@ -70,15 +65,10 @@ public static string GetDefaultName([NotNull] this IKey key) /// Returns the default key constraint name that would be used for this key for a particular table. /// /// The key. - /// The table name. - /// The schema. + /// The identifier of the containing store object. /// The default key constraint name that would be used for this key. - public static string GetDefaultName( - [NotNull] this IKey key, - [NotNull] string tableName, - [CanBeNull] string schema) + public static string GetDefaultName([NotNull] this IKey key, StoreObjectIdentifier storeObject) { - var storeObject = StoreObjectIdentifier.Table(tableName, schema); string name = null; if (key.IsPrimaryKey()) { @@ -100,10 +90,10 @@ public static string GetDefaultName( if (rootKey != null && rootKey != key) { - return rootKey.GetName(tableName, schema); + return rootKey.GetName(storeObject); } - name = "PK_" + tableName; + name = "PK_" + storeObject.Name; } else { @@ -128,12 +118,12 @@ public static string GetDefaultName( if (rootKey != key) { - return rootKey.GetName(tableName, schema); + return rootKey.GetName(storeObject); } name = new StringBuilder() .Append("AK_") - .Append(tableName) + .Append(storeObject.Name) .Append("_") .AppendJoin(key.Properties.Select(p => p.GetColumnName(storeObject)), "_") .ToString(); @@ -188,7 +178,7 @@ public static IEnumerable GetMappedConstraints([NotNull] this /// /// - /// Finds the first that is mapped to the same constraint in a shared table. + /// Finds the first that is mapped to the same constraint in a shared table-like object. /// /// /// This method is typically used by database providers (and other extensions). It is generally @@ -196,18 +186,13 @@ public static IEnumerable GetMappedConstraints([NotNull] this /// /// /// The key. - /// The table name. - /// The schema. + /// The identifier of the containing store object. /// The key found, or if none was found. - public static IKey FindSharedTableRootKey( - [NotNull] this IKey key, - [NotNull] string tableName, - [CanBeNull] string schema) + public static IKey FindSharedObjectRootKey([NotNull] this IKey key, StoreObjectIdentifier storeObject) { Check.NotNull(key, nameof(key)); - Check.NotNull(tableName, nameof(tableName)); - var keyName = key.GetName(tableName, schema); + var keyName = key.GetName(storeObject); var rootKey = key; // Limit traversal to avoid getting stuck in a cycle (validation will throw for these later) @@ -215,9 +200,9 @@ public static IKey FindSharedTableRootKey( for (var i = 0; i < Metadata.Internal.RelationalEntityTypeExtensions.MaxEntityTypesSharingTable; i++) { var linkedKey = rootKey.DeclaringEntityType - .FindRowInternalForeignKeys(StoreObjectIdentifier.Table(tableName, schema)) + .FindRowInternalForeignKeys(storeObject) .SelectMany(fk => fk.PrincipalEntityType.GetKeys()) - .FirstOrDefault(k => k.GetName(tableName, schema) == keyName); + .FirstOrDefault(k => k.GetName(storeObject) == keyName); if (linkedKey == null) { break; @@ -231,7 +216,7 @@ public static IKey FindSharedTableRootKey( /// /// - /// Finds the first that is mapped to the same constraint in a shared table. + /// Finds the first that is mapped to the same constraint in a shared table-like object. /// /// /// This method is typically used by database providers (and other extensions). It is generally @@ -239,18 +224,15 @@ public static IKey FindSharedTableRootKey( /// /// /// The key. - /// The table name. - /// The schema. + /// The identifier of the containing store object. /// The key found, or if none was found. - public static IMutableKey FindSharedTableRootKey( - [NotNull] this IMutableKey key, - [NotNull] string tableName, - [CanBeNull] string schema) - => (IMutableKey)((IKey)key).FindSharedTableRootKey(tableName, schema); + public static IMutableKey FindSharedObjectRootKey( + [NotNull] this IMutableKey key, StoreObjectIdentifier storeObject) + => (IMutableKey)((IKey)key).FindSharedObjectRootKey(storeObject); /// /// - /// Finds the first that is mapped to the same constraint in a shared table. + /// Finds the first that is mapped to the same constraint in a shared table-like object. /// /// /// This method is typically used by database providers (and other extensions). It is generally @@ -258,13 +240,10 @@ public static IMutableKey FindSharedTableRootKey( /// /// /// The key. - /// The table name. - /// The schema. + /// The identifier of the containing store object. /// The key found, or if none was found. - public static IConventionKey FindSharedTableRootKey( - [NotNull] this IConventionKey key, - [NotNull] string tableName, - [CanBeNull] string schema) - => (IConventionKey)((IKey)key).FindSharedTableRootKey(tableName, schema); + public static IConventionKey FindSharedObjectRootKey( + [NotNull] this IConventionKey key, StoreObjectIdentifier storeObject) + => (IConventionKey)((IKey)key).FindSharedObjectRootKey(storeObject); } } diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index a19c7f5d98b..850f72ce0e3 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -6,9 +6,7 @@ using System.Diagnostics; using System.Linq; using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; -using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Utilities; @@ -61,6 +59,7 @@ public override void Validate(IModel model, IDiagnosticsLogger(mappedTypes); IEntityType root = null; foreach (var mappedType in mappedTypes) @@ -332,7 +332,7 @@ protected virtual void ValidateSharedTableCompatibility( .PrincipalEntityType; throw new InvalidOperationException( RelationalStrings.IncompatibleTableDerivedRelationship( - Format(tableName, schema), + storeObject.DisplayName(), mappedType.DisplayName(), principalType.DisplayName())); } @@ -344,7 +344,7 @@ protected virtual void ValidateSharedTableCompatibility( { throw new InvalidOperationException( RelationalStrings.IncompatibleTableNoRelationship( - Format(tableName, schema), + storeObject.DisplayName(), mappedType.DisplayName(), root.DisplayName())); } @@ -372,16 +372,16 @@ protected virtual void ValidateSharedTableCompatibility( foreach (var nextEntityType in directlyConnectedTypes) { var otherKey = nextEntityType.FindPrimaryKey(); - if (key?.GetName(tableName, schema) != otherKey?.GetName(tableName, schema)) + if (key?.GetName(storeObject) != otherKey?.GetName(storeObject)) { throw new InvalidOperationException( RelationalStrings.IncompatibleTableKeyNameMismatch( - Format(tableName, schema), + storeObject.DisplayName(), entityType.DisplayName(), nextEntityType.DisplayName(), - key?.GetName(tableName, schema), + key?.GetName(storeObject), key?.Properties.Format(), - otherKey?.GetName(tableName, schema), + otherKey?.GetName(storeObject), otherKey?.Properties.Format())); } @@ -393,7 +393,7 @@ protected virtual void ValidateSharedTableCompatibility( { throw new InvalidOperationException( RelationalStrings.IncompatibleTableCommentMismatch( - Format(tableName, schema), + storeObject.DisplayName(), entityType.DisplayName(), nextEntityType.DisplayName(), comment, @@ -409,7 +409,7 @@ protected virtual void ValidateSharedTableCompatibility( { throw new InvalidOperationException( RelationalStrings.IncompatibleTableExcludedMismatch( - Format(tableName, schema), + storeObject.DisplayName(), entityType.DisplayName(), nextEntityType.DisplayName())); } @@ -439,6 +439,145 @@ protected virtual void ValidateSharedTableCompatibility( } } + /// + /// Validates the mapping/configuration of shared views in the model. + /// + /// The model to validate. + /// The logger to use. + protected virtual void ValidateSharedViewCompatibility( + [NotNull] IModel model, + [NotNull] IDiagnosticsLogger logger) + { + var views = new Dictionary>(); + foreach (var entityType in model.GetEntityTypes()) + { + var viewsName = entityType.GetViewName(); + if (viewsName == null) + { + continue; + } + + var view = StoreObjectIdentifier.View(viewsName, entityType.GetViewSchema()); + if (!views.TryGetValue(view, out var mappedTypes)) + { + mappedTypes = new List(); + views[view] = mappedTypes; + } + + mappedTypes.Add(entityType); + } + + foreach (var tableMapping in views) + { + var mappedTypes = tableMapping.Value; + var table = tableMapping.Key; + ValidateSharedViewCompatibility(mappedTypes, table.Name, table.Schema, logger); + ValidateSharedColumnsCompatibility(mappedTypes, table, logger); + } + } + + /// + /// Validates the compatibility of entity types sharing a given view. + /// + /// The mapped entity types. + /// The view name. + /// The schema. + /// The logger to use. + protected virtual void ValidateSharedViewCompatibility( + [NotNull] IReadOnlyList mappedTypes, + [NotNull] string viewName, + [CanBeNull] string schema, + [NotNull] IDiagnosticsLogger logger) + { + if (mappedTypes.Count == 1) + { + return; + } + + var storeObject = StoreObjectIdentifier.View(viewName, schema); + var unvalidatedTypes = new HashSet(mappedTypes); + IEntityType root = null; + foreach (var mappedType in mappedTypes) + { + if (mappedType.BaseType != null && unvalidatedTypes.Contains(mappedType.BaseType)) + { + continue; + } + + if (mappedType.FindPrimaryKey() != null + && mappedType.FindForeignKeys(mappedType.FindPrimaryKey().Properties) + .Any(fk => fk.PrincipalKey.IsPrimaryKey() + && unvalidatedTypes.Contains(fk.PrincipalEntityType))) + { + if (mappedType.BaseType != null) + { + var principalType = mappedType.FindForeignKeys(mappedType.FindPrimaryKey().Properties) + .First(fk => fk.PrincipalKey.IsPrimaryKey() + && unvalidatedTypes.Contains(fk.PrincipalEntityType)) + .PrincipalEntityType; + throw new InvalidOperationException( + RelationalStrings.IncompatibleViewDerivedRelationship( + storeObject.DisplayName(), + mappedType.DisplayName(), + principalType.DisplayName())); + } + + continue; + } + + if (root != null) + { + throw new InvalidOperationException( + RelationalStrings.IncompatibleViewNoRelationship( + storeObject.DisplayName(), + mappedType.DisplayName(), + root.DisplayName())); + } + + root = mappedType; + } + + Check.DebugAssert(root != null, "root != null"); + unvalidatedTypes.Remove(root); + var typesToValidate = new Queue(); + typesToValidate.Enqueue(root); + + while (typesToValidate.Count > 0) + { + var entityType = typesToValidate.Dequeue(); + var typesToValidateLeft = typesToValidate.Count; + var directlyConnectedTypes = unvalidatedTypes.Where( + unvalidatedType => + entityType.IsAssignableFrom(unvalidatedType) + || IsIdentifyingPrincipal(unvalidatedType, entityType)); + + foreach (var nextEntityType in directlyConnectedTypes) + { + typesToValidate.Enqueue(nextEntityType); + } + + foreach (var typeToValidate in typesToValidate.Skip(typesToValidateLeft)) + { + unvalidatedTypes.Remove(typeToValidate); + } + } + + if (unvalidatedTypes.Count == 0) + { + return; + } + + foreach (var invalidEntityType in unvalidatedTypes) + { + Check.DebugAssert(root != null, "root is null"); + throw new InvalidOperationException( + RelationalStrings.IncompatibleViewNoRelationship( + viewName, + invalidEntityType.DisplayName(), + root.DisplayName())); + } + } + private static bool IsIdentifyingPrincipal(IEntityType dependentEntityType, IEntityType principalEntityType) => dependentEntityType.FindForeignKeys(dependentEntityType.FindPrimaryKey().Properties) .Any(fk => fk.PrincipalKey.IsPrimaryKey() @@ -457,7 +596,8 @@ protected virtual void ValidateSharedColumnsCompatibility( { Dictionary storeConcurrencyTokens = null; HashSet missingConcurrencyTokens = null; - if (mappedTypes.Count > 1) + if (mappedTypes.Count > 1 + && storeObject.StoreObjectType == StoreObjectType.Table) { foreach (var property in mappedTypes.SelectMany(et => et.GetDeclaredProperties())) { @@ -752,13 +892,11 @@ protected virtual object GetDefaultColumnValue( /// Validates the compatibility of foreign keys in a given shared table. /// /// The mapped entity types. - /// The table name. - /// The schema. + /// The identifier of the store object. /// The logger to use. protected virtual void ValidateSharedForeignKeysCompatibility( [NotNull] IReadOnlyList mappedTypes, - [NotNull] string tableName, - [CanBeNull] string schema, + StoreObjectIdentifier storeObject, [NotNull] IDiagnosticsLogger logger) { var foreignKeyMappings = new Dictionary(); @@ -766,14 +904,15 @@ protected virtual void ValidateSharedForeignKeysCompatibility( foreach (var foreignKey in mappedTypes.SelectMany(et => et.GetDeclaredForeignKeys())) { var foreignKeyName = foreignKey.GetConstraintName( - tableName, schema, foreignKey.PrincipalEntityType.GetTableName(), foreignKey.PrincipalEntityType.GetSchema()); + storeObject, + StoreObjectIdentifier.Table(foreignKey.PrincipalEntityType.GetTableName(), foreignKey.PrincipalEntityType.GetSchema())); if (!foreignKeyMappings.TryGetValue(foreignKeyName, out var duplicateForeignKey)) { foreignKeyMappings[foreignKeyName] = foreignKey; continue; } - ValidateCompatible(foreignKey, duplicateForeignKey, foreignKeyName, tableName, schema, logger); + ValidateCompatible(foreignKey, duplicateForeignKey, foreignKeyName, storeObject, logger); } } @@ -783,43 +922,38 @@ protected virtual void ValidateSharedForeignKeysCompatibility( /// A foreign key. /// Another foreign key. /// The foreign key constraint name. - /// The table name. - /// The schema. + /// The identifier of the store object. /// The logger to use. protected virtual void ValidateCompatible( [NotNull] IForeignKey foreignKey, [NotNull] IForeignKey duplicateForeignKey, [NotNull] string foreignKeyName, - [NotNull] string tableName, - [CanBeNull] string schema, + StoreObjectIdentifier storeObject, [NotNull] IDiagnosticsLogger logger) - => foreignKey.AreCompatible(duplicateForeignKey, tableName, schema, shouldThrow: true); + => foreignKey.AreCompatible(duplicateForeignKey, storeObject, shouldThrow: true); /// /// Validates the compatibility of indexes in a given shared table. /// /// The mapped entity types. - /// The table name. - /// The schema. + /// The identifier of the store object. /// The logger to use. protected virtual void ValidateSharedIndexesCompatibility( [NotNull] IReadOnlyList mappedTypes, - [NotNull] string tableName, - [CanBeNull] string schema, + StoreObjectIdentifier storeObject, [NotNull] IDiagnosticsLogger logger) { var indexMappings = new Dictionary(); - foreach (var index in mappedTypes.SelectMany(et => et.GetDeclaredIndexes())) { - var indexName = index.GetDatabaseName(tableName, schema); + var indexName = index.GetDatabaseName(storeObject); if (!indexMappings.TryGetValue(indexName, out var duplicateIndex)) { indexMappings[indexName] = index; continue; } - ValidateCompatible(index, duplicateIndex, indexName, tableName, schema, logger); + ValidateCompatible(index, duplicateIndex, indexName, storeObject, logger); } } @@ -829,36 +963,31 @@ protected virtual void ValidateSharedIndexesCompatibility( /// An index. /// Another index. /// The name of the index. - /// The table name. - /// The schema. + /// The identifier of the store object. /// The logger to use. protected virtual void ValidateCompatible( [NotNull] IIndex index, [NotNull] IIndex duplicateIndex, [NotNull] string indexName, - [NotNull] string tableName, - [CanBeNull] string schema, + StoreObjectIdentifier storeObject, [NotNull] IDiagnosticsLogger logger) - => index.AreCompatible(duplicateIndex, tableName, schema, shouldThrow: true); + => index.AreCompatible(duplicateIndex, storeObject, shouldThrow: true); /// /// Validates the compatibility of primary and alternate keys in a given shared table. /// /// The mapped entity types. - /// The table name. - /// The schema. + /// The identifier of the store object. /// The logger to use. protected virtual void ValidateSharedKeysCompatibility( [NotNull] IReadOnlyList mappedTypes, - [NotNull] string tableName, - [CanBeNull] string schema, + StoreObjectIdentifier storeObject, [NotNull] IDiagnosticsLogger logger) { var keyMappings = new Dictionary(); - foreach (var key in mappedTypes.SelectMany(et => et.GetDeclaredKeys())) { - var keyName = key.GetName(tableName, schema); + var keyName = key.GetName(storeObject); if (!keyMappings.TryGetValue(keyName, out var duplicateKey)) { @@ -866,7 +995,7 @@ protected virtual void ValidateSharedKeysCompatibility( continue; } - ValidateCompatible(key, duplicateKey, keyName, tableName, schema, logger); + ValidateCompatible(key, duplicateKey, keyName, storeObject, logger); } } @@ -876,18 +1005,16 @@ protected virtual void ValidateSharedKeysCompatibility( /// A key. /// Another key. /// The name of the unique constraint. - /// The table name. - /// The schema. + /// The identifier of the store object. /// The logger to use. protected virtual void ValidateCompatible( [NotNull] IKey key, [NotNull] IKey duplicateKey, [NotNull] string keyName, - [NotNull] string tableName, - [CanBeNull] string schema, + StoreObjectIdentifier storeObject, [NotNull] IDiagnosticsLogger logger) { - key.AreCompatible(duplicateKey, tableName, schema, shouldThrow: true); + key.AreCompatible(duplicateKey, storeObject, shouldThrow: true); } /// @@ -1049,9 +1176,6 @@ protected virtual void ValidatePropertyOverrides( } } - private static string Format(string tableName, string schema) - => schema == null ? tableName : schema + "." + tableName; - /// /// Validates that the properties of any one index are /// all mapped to columns on at least one common table. diff --git a/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs b/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs index 391010ac7a1..ba79f94ef15 100644 --- a/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs @@ -54,13 +54,13 @@ public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, keys.Clear(); foreignKeys.Clear(); - var (tableName, schema) = table.Key; + var storeObject = StoreObjectIdentifier.Table(table.Key.TableName, table.Key.Schema); foreach (var entityType in table.Value) { - TryUniquifyColumnNames(entityType, columns, tableName, schema, maxLength); - TryUniquifyKeyNames(entityType, keys, tableName, schema, maxLength); - TryUniquifyForeignKeyNames(entityType, foreignKeys, tableName, schema, maxLength); - TryUniquifyIndexNames(entityType, indexes, tableName, schema, maxLength); + TryUniquifyColumnNames(entityType, columns, storeObject, maxLength); + TryUniquifyKeyNames(entityType, keys, storeObject, maxLength); + TryUniquifyForeignKeyNames(entityType, foreignKeys, storeObject, maxLength); + TryUniquifyIndexNames(entityType, indexes, storeObject, maxLength); } } } @@ -152,13 +152,11 @@ private static void TryUniquifyTableNames( private static void TryUniquifyColumnNames( IConventionEntityType entityType, Dictionary properties, - string tableName, - string schema, + StoreObjectIdentifier storeObject, int maxLength) { foreach (var property in entityType.GetDeclaredProperties()) { - var storeObject = StoreObjectIdentifier.Table(tableName, schema); var columnName = property.GetColumnName(storeObject); if (columnName == null) { @@ -236,13 +234,12 @@ private static string TryUniquify( private void TryUniquifyKeyNames( IConventionEntityType entityType, Dictionary keys, - string tableName, - string schema, + StoreObjectIdentifier storeObject, int maxLength) { foreach (var key in entityType.GetDeclaredKeys()) { - var keyName = key.GetName(tableName, schema); + var keyName = key.GetName(storeObject); if (!keys.TryGetValue(keyName, out var otherKey)) { keys[keyName] = key; @@ -251,7 +248,7 @@ private void TryUniquifyKeyNames( if ((key.IsPrimaryKey() && otherKey.IsPrimaryKey()) - || AreCompatible(key, otherKey, tableName, schema)) + || AreCompatible(key, otherKey, storeObject)) { continue; } @@ -277,15 +274,13 @@ private void TryUniquifyKeyNames( /// /// A key. /// Another key. - /// The table name. - /// The schema. + /// The identifier of the store object. /// if compatible protected virtual bool AreCompatible( [NotNull] IKey key, [NotNull] IKey duplicateKey, - [NotNull] string tableName, - [CanBeNull] string schema) - => key.AreCompatible(duplicateKey, tableName, schema, shouldThrow: false); + StoreObjectIdentifier storeObject) + => key.AreCompatible(duplicateKey, storeObject, shouldThrow: false); private static string TryUniquify( IConventionKey key, string keyName, Dictionary keys, int maxLength) @@ -303,20 +298,19 @@ private static string TryUniquify( private void TryUniquifyIndexNames( IConventionEntityType entityType, Dictionary indexes, - string tableName, - string schema, + StoreObjectIdentifier storeObject, int maxLength) { foreach (var index in entityType.GetDeclaredIndexes()) { - var indexName = index.GetDatabaseName(tableName, schema); + var indexName = index.GetDatabaseName(storeObject); if (!indexes.TryGetValue(indexName, out var otherIndex)) { indexes[indexName] = index; continue; } - if (AreCompatible(index, otherIndex, tableName, schema)) + if (AreCompatible(index, otherIndex, storeObject)) { continue; } @@ -342,15 +336,13 @@ private void TryUniquifyIndexNames( /// /// An index. /// Another index. - /// The table name. - /// The schema. + /// The identifier of the store object. /// if compatible protected virtual bool AreCompatible( [NotNull] IIndex index, [NotNull] IIndex duplicateIndex, - [NotNull] string tableName, - [CanBeNull] string schema) - => index.AreCompatible(duplicateIndex, tableName, schema, shouldThrow: false); + StoreObjectIdentifier storeObject) + => index.AreCompatible(duplicateIndex, storeObject, shouldThrow: false); private static string TryUniquify( IConventionIndex index, string indexName, Dictionary indexes, int maxLength) @@ -368,8 +360,7 @@ private static string TryUniquify( private void TryUniquifyForeignKeyNames( IConventionEntityType entityType, Dictionary foreignKeys, - string tableName, - string schema, + StoreObjectIdentifier storeObject, int maxLength) { foreach (var foreignKey in entityType.GetDeclaredForeignKeys()) @@ -380,15 +371,16 @@ private void TryUniquifyForeignKeyNames( continue; } - var foreignKeyName = foreignKey.GetConstraintName(tableName, schema, - foreignKey.PrincipalEntityType.GetTableName(), foreignKey.PrincipalEntityType.GetSchema()); + var foreignKeyName = foreignKey.GetConstraintName(storeObject, + StoreObjectIdentifier.Table(foreignKey.PrincipalEntityType.GetTableName(), + foreignKey.PrincipalEntityType.GetSchema())); if (!foreignKeys.TryGetValue(foreignKeyName, out var otherForeignKey)) { foreignKeys[foreignKeyName] = foreignKey; continue; } - if (AreCompatible(foreignKey, otherForeignKey, tableName, schema)) + if (AreCompatible(foreignKey, otherForeignKey, storeObject)) { continue; } @@ -414,15 +406,13 @@ private void TryUniquifyForeignKeyNames( /// /// A foreign key. /// Another foreign key. - /// The table name. - /// The schema. + /// The identifier of the store object. /// if compatible protected virtual bool AreCompatible( [NotNull] IForeignKey foreignKey, [NotNull] IForeignKey duplicateForeignKey, - [NotNull] string tableName, - [CanBeNull] string schema) - => foreignKey.AreCompatible(duplicateForeignKey, tableName, schema, shouldThrow: false); + StoreObjectIdentifier storeObject) + => foreignKey.AreCompatible(duplicateForeignKey, storeObject, shouldThrow: false); private static string TryUniquify( IConventionForeignKey foreignKey, string foreignKeyName, Dictionary foreignKeys, int maxLength) diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalForeignKeyExtensions.cs b/src/EFCore.Relational/Metadata/Internal/RelationalForeignKeyExtensions.cs index b5a1db89134..2931f39b4f3 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalForeignKeyExtensions.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalForeignKeyExtensions.cs @@ -26,8 +26,7 @@ public static class RelationalForeignKeyExtensions public static bool AreCompatible( [NotNull] this IForeignKey foreignKey, [NotNull] IForeignKey duplicateForeignKey, - [NotNull] string tableName, - [CanBeNull] string schema, + StoreObjectIdentifier storeObject, bool shouldThrow) { var principalType = foreignKey.PrincipalEntityType; @@ -44,8 +43,9 @@ public static bool AreCompatible( duplicateForeignKey.Properties.Format(), duplicateForeignKey.DeclaringEntityType.DisplayName(), foreignKey.DeclaringEntityType.GetSchemaQualifiedTableName(), - foreignKey.GetConstraintName(tableName, schema, - foreignKey.PrincipalEntityType.GetTableName(), foreignKey.PrincipalEntityType.GetSchema()), + foreignKey.GetConstraintName(storeObject, + StoreObjectIdentifier.Table(foreignKey.PrincipalEntityType.GetTableName(), + foreignKey.PrincipalEntityType.GetSchema())), principalType.GetSchemaQualifiedTableName(), duplicatePrincipalType.GetSchemaQualifiedTableName())); } @@ -53,7 +53,6 @@ public static bool AreCompatible( return false; } - var storeObject = StoreObjectIdentifier.Table(tableName, schema); if (!foreignKey.Properties.Select(p => p.GetColumnName(storeObject)) .SequenceEqual(duplicateForeignKey.Properties.Select(p => p.GetColumnName(storeObject)))) { @@ -66,8 +65,9 @@ public static bool AreCompatible( duplicateForeignKey.Properties.Format(), duplicateForeignKey.DeclaringEntityType.DisplayName(), foreignKey.DeclaringEntityType.GetSchemaQualifiedTableName(), - foreignKey.GetConstraintName(tableName, schema, - foreignKey.PrincipalEntityType.GetTableName(), foreignKey.PrincipalEntityType.GetSchema()), + foreignKey.GetConstraintName(storeObject, + StoreObjectIdentifier.Table(foreignKey.PrincipalEntityType.GetTableName(), + foreignKey.PrincipalEntityType.GetSchema())), foreignKey.Properties.FormatColumns(storeObject), duplicateForeignKey.Properties.FormatColumns(storeObject))); } @@ -87,8 +87,9 @@ public static bool AreCompatible( duplicateForeignKey.Properties.Format(), duplicateForeignKey.DeclaringEntityType.DisplayName(), foreignKey.DeclaringEntityType.GetSchemaQualifiedTableName(), - foreignKey.GetConstraintName(tableName, schema, - foreignKey.PrincipalEntityType.GetTableName(), foreignKey.PrincipalEntityType.GetSchema()), + foreignKey.GetConstraintName(storeObject, + StoreObjectIdentifier.Table(foreignKey.PrincipalEntityType.GetTableName(), + foreignKey.PrincipalEntityType.GetSchema())), foreignKey.PrincipalKey.Properties.FormatColumns(storeObject), duplicateForeignKey.PrincipalKey.Properties.FormatColumns(storeObject))); } @@ -107,8 +108,9 @@ public static bool AreCompatible( duplicateForeignKey.Properties.Format(), duplicateForeignKey.DeclaringEntityType.DisplayName(), foreignKey.DeclaringEntityType.GetSchemaQualifiedTableName(), - foreignKey.GetConstraintName(tableName, schema, - foreignKey.PrincipalEntityType.GetTableName(), foreignKey.PrincipalEntityType.GetSchema()))); + foreignKey.GetConstraintName(storeObject, + StoreObjectIdentifier.Table(foreignKey.PrincipalEntityType.GetTableName(), + foreignKey.PrincipalEntityType.GetSchema())))); } return false; @@ -125,8 +127,9 @@ public static bool AreCompatible( duplicateForeignKey.Properties.Format(), duplicateForeignKey.DeclaringEntityType.DisplayName(), foreignKey.DeclaringEntityType.GetSchemaQualifiedTableName(), - foreignKey.GetConstraintName(tableName, schema, - foreignKey.PrincipalEntityType.GetTableName(), foreignKey.PrincipalEntityType.GetSchema()), + foreignKey.GetConstraintName(storeObject, + StoreObjectIdentifier.Table(foreignKey.PrincipalEntityType.GetTableName(), + foreignKey.PrincipalEntityType.GetSchema())), foreignKey.DeleteBehavior, duplicateForeignKey.DeleteBehavior)); } diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalIndexExtensions.cs b/src/EFCore.Relational/Metadata/Internal/RelationalIndexExtensions.cs index 1f184dbd2dc..66a2c9343f1 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalIndexExtensions.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalIndexExtensions.cs @@ -26,11 +26,9 @@ public static class RelationalIndexExtensions public static bool AreCompatible( [NotNull] this IIndex index, [NotNull] IIndex duplicateIndex, - [NotNull] string tableName, - [CanBeNull] string schema, + StoreObjectIdentifier storeObject, bool shouldThrow) { - var storeObject = StoreObjectIdentifier.Table(tableName, schema); if (!index.Properties.Select(p => p.GetColumnName(storeObject)) .SequenceEqual(duplicateIndex.Properties.Select(p => p.GetColumnName(storeObject)))) { @@ -43,7 +41,7 @@ public static bool AreCompatible( duplicateIndex.Properties.Format(), duplicateIndex.DeclaringEntityType.DisplayName(), index.DeclaringEntityType.GetSchemaQualifiedTableName(), - index.GetDatabaseName(tableName, schema), + index.GetDatabaseName(storeObject), index.Properties.FormatColumns(storeObject), duplicateIndex.Properties.FormatColumns(storeObject))); } @@ -62,7 +60,7 @@ public static bool AreCompatible( duplicateIndex.Properties.Format(), duplicateIndex.DeclaringEntityType.DisplayName(), index.DeclaringEntityType.GetSchemaQualifiedTableName(), - index.GetDatabaseName(tableName, schema))); + index.GetDatabaseName(storeObject))); } return false; diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalKeyExtensions.cs b/src/EFCore.Relational/Metadata/Internal/RelationalKeyExtensions.cs index 29926a43ef0..46b66f5dd2b 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalKeyExtensions.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalKeyExtensions.cs @@ -26,11 +26,9 @@ public static class RelationalKeyExtensions public static bool AreCompatible( [NotNull] this IKey key, [NotNull] IKey duplicateKey, - [NotNull] string tableName, - [CanBeNull] string schema, + StoreObjectIdentifier storeObject, bool shouldThrow) { - var storeObject = StoreObjectIdentifier.Table(tableName, schema); if (!key.Properties.Select(p => p.GetColumnName(storeObject)) .SequenceEqual(duplicateKey.Properties.Select(p => p.GetColumnName(storeObject)))) { @@ -43,7 +41,7 @@ public static bool AreCompatible( duplicateKey.Properties.Format(), duplicateKey.DeclaringEntityType.DisplayName(), key.DeclaringEntityType.GetSchemaQualifiedTableName(), - key.GetName(tableName, schema), + key.GetName(storeObject), key.Properties.FormatColumns(storeObject), duplicateKey.Properties.FormatColumns(storeObject))); } diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs index 76afd038dbd..bbd6b61c135 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs @@ -745,6 +745,7 @@ private static StoreFunction GetOrCreateStoreFunction(DbFunction dbFunction, Rel private static void PopulateConstraints(Table table) { + var storeObject = StoreObjectIdentifier.Table(table.Name, table.Schema); foreach (var entityTypeMapping in ((ITable)table).EntityTypeMappings) { var entityType = (IConventionEntityType)entityTypeMapping.EntityType; @@ -760,7 +761,8 @@ private static void PopulateConstraints(Table table) } var principalTable = (Table)principalMapping.Table; - var name = foreignKey.GetConstraintName(table.Name, table.Schema, principalTable.Name, principalTable.Schema); + var name = foreignKey.GetConstraintName(storeObject, + StoreObjectIdentifier.Table(principalTable.Name, principalTable.Schema)); if (name == null) { continue; @@ -837,7 +839,7 @@ private static void PopulateConstraints(Table table) foreach (var key in entityType.GetKeys()) { - var name = key.GetName(table.Name, table.Schema); + var name = key.GetName(storeObject); var constraint = table.FindUniqueConstraint(name); if (constraint == null) { @@ -878,7 +880,7 @@ private static void PopulateConstraints(Table table) foreach (var index in entityType.GetIndexes()) { - var name = index.GetDatabaseName(table.Name, table.Schema); + var name = index.GetDatabaseName(storeObject); if (!table.Indexes.TryGetValue(name, out var tableIndex)) { var columns = new Column[index.Properties.Count]; @@ -897,7 +899,7 @@ private static void PopulateConstraints(Table table) continue; } - tableIndex = new TableIndex(name, table, columns, index.GetFilter(table.Name, table.Schema), index.IsUnique); + tableIndex = new TableIndex(name, table, columns, index.GetFilter(storeObject), index.IsUnique); table.Indexes.Add(name, tableIndex); } diff --git a/src/EFCore.Relational/Metadata/StoreObjectIdentifier.cs b/src/EFCore.Relational/Metadata/StoreObjectIdentifier.cs index ebdb032ce6d..5bd5c302171 100644 --- a/src/EFCore.Relational/Metadata/StoreObjectIdentifier.cs +++ b/src/EFCore.Relational/Metadata/StoreObjectIdentifier.cs @@ -3,7 +3,6 @@ using System; using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Metadata @@ -76,30 +75,18 @@ public static StoreObjectIdentifier DbFunction([NotNull] string modelName) } /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// Gets the table-like store object type. /// - [EntityFrameworkInternal] public StoreObjectType StoreObjectType { get; private 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. + /// Gets the table-like store object name. /// - [EntityFrameworkInternal] public string Name { get; private 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. + /// Gets the table-like store object schema. /// - [EntityFrameworkInternal] public string Schema { get; private set; } /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 6bb0eb38f85..eae96bf8a42 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -1,4 +1,3 @@ - // using System; @@ -974,6 +973,22 @@ public static string ViewOverrideMismatch([CanBeNull] object propertySpecificati public static string SequenceContainsNoElements => GetString("SequenceContainsNoElements"); + /// + /// Cannot use view '{view}' for entity type '{entityType}' since it is being used for entity type '{otherEntityType}', there is a relationship between their primary keys in which '{entityType}' is the dependent and '{entityType}' has a base entity type mapped to a different view. Either map '{otherEntityType}' to a different view or invert the relationship between '{entityType}' and '{otherEntityType}'. + /// + public static string IncompatibleViewDerivedRelationship([CanBeNull] object view, [CanBeNull] object entityType, [CanBeNull] object otherEntityType) + => string.Format( + GetString("IncompatibleViewDerivedRelationship", nameof(view), nameof(entityType), nameof(otherEntityType)), + view, entityType, otherEntityType); + + /// + /// Cannot use view '{view}' for entity type '{entityType}' since it is being used for entity type '{otherEntityType}' and there is no relationship between their primary keys. + /// + public static string IncompatibleViewNoRelationship([CanBeNull] object view, [CanBeNull] object entityType, [CanBeNull] object otherEntityType) + => string.Format( + GetString("IncompatibleViewNoRelationship", nameof(view), nameof(entityType), nameof(otherEntityType)), + view, entityType, otherEntityType); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 6389362bc0f..bf6ae3cbce1 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -708,4 +708,10 @@ An error occurred while the batch executor was releasing a transaction savepoint. Debug RelationalEventId.BatchExecutorFailedToReleaseSavepoint + + Cannot use view '{view}' for entity type '{entityType}' since it is being used for entity type '{otherEntityType}', there is a relationship between their primary keys in which '{entityType}' is the dependent and '{entityType}' has a base entity type mapped to a different view. Either map '{otherEntityType}' to a different view or invert the relationship between '{entityType}' and '{otherEntityType}'. + + + Cannot use view '{view}' for entity type '{entityType}' since it is being used for entity type '{otherEntityType}' and there is no relationship between their primary keys. + \ No newline at end of file diff --git a/src/EFCore.SqlServer/Extensions/SqlServerIndexExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerIndexExtensions.cs index 689de4f6d6c..ef4b3b6f21e 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerIndexExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerIndexExtensions.cs @@ -27,13 +27,9 @@ public static class SqlServerIndexExtensions /// Returns a value indicating whether the index is clustered. /// /// The index. - /// The table name. - /// The schema. + /// The identifier of the store object. /// if the index is clustered. - public static bool? IsClustered( - [NotNull] this IIndex index, - [NotNull] string tableName, - [CanBeNull] string schema) + public static bool? IsClustered([NotNull] this IIndex index, StoreObjectIdentifier storeObject) { var annotation = index.FindAnnotation(SqlServerAnnotationNames.Clustered); if (annotation != null) @@ -41,16 +37,13 @@ public static class SqlServerIndexExtensions return (bool?)annotation.Value; } - return GetDefaultIsClustered(index, tableName, schema); + return GetDefaultIsClustered(index, storeObject); } - private static bool? GetDefaultIsClustered( - [NotNull] IIndex index, - [NotNull] string tableName, - [CanBeNull] string schema) + private static bool? GetDefaultIsClustered([NotNull] IIndex index, StoreObjectIdentifier storeObject) { - var sharedTableRootIndex = index.FindSharedTableRootIndex(tableName, schema); - return sharedTableRootIndex?.IsClustered(tableName, schema); + var sharedTableRootIndex = index.FindSharedObjectRootIndex(storeObject); + return sharedTableRootIndex?.IsClustered(storeObject); } /// diff --git a/src/EFCore.SqlServer/Extensions/SqlServerKeyExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerKeyExtensions.cs index 277a487e754..9103d379d16 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerKeyExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerKeyExtensions.cs @@ -25,13 +25,9 @@ public static class SqlServerKeyExtensions /// Returns a value indicating whether the key is clustered. /// /// The key. - /// The table name. - /// The schema. + /// The identifier of the store object. /// if the key is clustered. - public static bool? IsClustered( - [NotNull] this IKey key, - [NotNull] string tableName, - [CanBeNull] string schema) + public static bool? IsClustered([NotNull] this IKey key, StoreObjectIdentifier storeObject) { var annotation = key.FindAnnotation(SqlServerAnnotationNames.Clustered); if (annotation != null) @@ -39,16 +35,13 @@ public static class SqlServerKeyExtensions return (bool?)annotation.Value; } - return GetDefaultIsClustered(key, tableName, schema); + return GetDefaultIsClustered(key, storeObject); } - private static bool? GetDefaultIsClustered( - [NotNull] IKey key, - [NotNull] string tableName, - [CanBeNull] string schema) + private static bool? GetDefaultIsClustered([NotNull] IKey key, StoreObjectIdentifier storeObject) { - var sharedTableRootKey = key.FindSharedTableRootKey(tableName, schema); - return sharedTableRootKey?.IsClustered(tableName, schema); + var sharedTableRootKey = key.FindSharedObjectRootKey(storeObject); + return sharedTableRootKey?.IsClustered(storeObject); } /// diff --git a/src/EFCore.SqlServer/Internal/SqlServerModelValidator.cs b/src/EFCore.SqlServer/Internal/SqlServerModelValidator.cs index e4b0159b3f7..02f31e097ad 100644 --- a/src/EFCore.SqlServer/Internal/SqlServerModelValidator.cs +++ b/src/EFCore.SqlServer/Internal/SqlServerModelValidator.cs @@ -333,13 +333,12 @@ protected override void ValidateCompatible( IKey key, IKey duplicateKey, string keyName, - string tableName, - string schema, + StoreObjectIdentifier storeObject, IDiagnosticsLogger logger) { - base.ValidateCompatible(key, duplicateKey, keyName, tableName, schema, logger); + base.ValidateCompatible(key, duplicateKey, keyName, storeObject, logger); - key.AreCompatibleForSqlServer(duplicateKey, tableName, schema, shouldThrow: true); + key.AreCompatibleForSqlServer(duplicateKey, storeObject, shouldThrow: true); } /// @@ -347,13 +346,12 @@ protected override void ValidateCompatible( IIndex index, IIndex duplicateIndex, string indexName, - string tableName, - string schema, + StoreObjectIdentifier storeObject, IDiagnosticsLogger logger) { - base.ValidateCompatible(index, duplicateIndex, indexName, tableName, schema, logger); + base.ValidateCompatible(index, duplicateIndex, indexName, storeObject, logger); - index.AreCompatibleForSqlServer(duplicateIndex, tableName, schema, shouldThrow: true); + index.AreCompatibleForSqlServer(duplicateIndex, storeObject, shouldThrow: true); } } } diff --git a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerSharedTableConvention.cs b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerSharedTableConvention.cs index 6dc15a74198..1b213d12f12 100644 --- a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerSharedTableConvention.cs +++ b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerSharedTableConvention.cs @@ -31,13 +31,13 @@ public SqlServerSharedTableConvention( } /// - protected override bool AreCompatible(IKey key, IKey duplicateKey, string tableName, string schema) - => base.AreCompatible(key, duplicateKey, tableName, schema) - && key.AreCompatibleForSqlServer(duplicateKey, tableName, schema, shouldThrow: false); + protected override bool AreCompatible(IKey key, IKey duplicateKey, StoreObjectIdentifier storeObject) + => base.AreCompatible(key, duplicateKey, storeObject) + && key.AreCompatibleForSqlServer(duplicateKey, storeObject, shouldThrow: false); /// - protected override bool AreCompatible(IIndex index, IIndex duplicateIndex, string tableName, string schema) - => base.AreCompatible(index, duplicateIndex, tableName, schema) - && index.AreCompatibleForSqlServer(duplicateIndex, tableName, schema, shouldThrow: false); + protected override bool AreCompatible(IIndex index, IIndex duplicateIndex, StoreObjectIdentifier storeObject) + => base.AreCompatible(index, duplicateIndex, storeObject) + && index.AreCompatibleForSqlServer(duplicateIndex, storeObject, shouldThrow: false); } } diff --git a/src/EFCore.SqlServer/Metadata/Internal/SqlServerIndexExtensions.cs b/src/EFCore.SqlServer/Metadata/Internal/SqlServerIndexExtensions.cs index e1e5d2add6d..b672bda79e6 100644 --- a/src/EFCore.SqlServer/Metadata/Internal/SqlServerIndexExtensions.cs +++ b/src/EFCore.SqlServer/Metadata/Internal/SqlServerIndexExtensions.cs @@ -25,8 +25,7 @@ public static class SqlServerIndexExtensions public static bool AreCompatibleForSqlServer( [NotNull] this IIndex index, [NotNull] IIndex duplicateIndex, - [NotNull] string tableName, - [CanBeNull] string schema, + StoreObjectIdentifier storeObject, bool shouldThrow) { if (index.GetIncludeProperties() != duplicateIndex.GetIncludeProperties()) @@ -34,11 +33,11 @@ public static bool AreCompatibleForSqlServer( if (index.GetIncludeProperties() == null || duplicateIndex.GetIncludeProperties() == null || !index.GetIncludeProperties().Select( - p => index.DeclaringEntityType.FindProperty(p).GetColumnName(StoreObjectIdentifier.Table(tableName, schema))) + p => index.DeclaringEntityType.FindProperty(p).GetColumnName(storeObject)) .SequenceEqual( duplicateIndex.GetIncludeProperties().Select( p => duplicateIndex.DeclaringEntityType.FindProperty(p) - .GetColumnName(StoreObjectIdentifier.Table(tableName, schema))))) + .GetColumnName(storeObject)))) { if (shouldThrow) { @@ -49,9 +48,9 @@ public static bool AreCompatibleForSqlServer( duplicateIndex.Properties.Format(), duplicateIndex.DeclaringEntityType.DisplayName(), index.DeclaringEntityType.GetSchemaQualifiedTableName(), - index.GetDatabaseName(tableName, schema), - FormatInclude(index, tableName, schema), - FormatInclude(duplicateIndex, tableName, schema))); + index.GetDatabaseName(storeObject), + FormatInclude(index, storeObject), + FormatInclude(duplicateIndex, storeObject))); } return false; @@ -69,13 +68,13 @@ public static bool AreCompatibleForSqlServer( duplicateIndex.Properties.Format(), duplicateIndex.DeclaringEntityType.DisplayName(), index.DeclaringEntityType.GetSchemaQualifiedTableName(), - index.GetDatabaseName(tableName, schema))); + index.GetDatabaseName(storeObject))); } return false; } - if (index.IsClustered(tableName, schema) != duplicateIndex.IsClustered(tableName, schema)) + if (index.IsClustered(storeObject) != duplicateIndex.IsClustered(storeObject)) { if (shouldThrow) { @@ -86,7 +85,7 @@ public static bool AreCompatibleForSqlServer( duplicateIndex.Properties.Format(), duplicateIndex.DeclaringEntityType.DisplayName(), index.DeclaringEntityType.GetSchemaQualifiedTableName(), - index.GetDatabaseName(tableName, schema))); + index.GetDatabaseName(storeObject))); } return false; @@ -103,7 +102,7 @@ public static bool AreCompatibleForSqlServer( duplicateIndex.Properties.Format(), duplicateIndex.DeclaringEntityType.DisplayName(), index.DeclaringEntityType.GetSchemaQualifiedTableName(), - index.GetDatabaseName(tableName, schema))); + index.GetDatabaseName(storeObject))); } return false; @@ -112,13 +111,13 @@ public static bool AreCompatibleForSqlServer( return true; } - private static string FormatInclude(IIndex index, string tableName, string schema) + private static string FormatInclude(IIndex index, StoreObjectIdentifier storeObject) => index.GetIncludeProperties() == null ? "{}" : "{'" + string.Join("', '", index.GetIncludeProperties().Select(p => index.DeclaringEntityType.FindProperty(p) - ?.GetColumnName(StoreObjectIdentifier.Table(tableName, schema)))) + ?.GetColumnName(storeObject))) + "'}"; } } diff --git a/src/EFCore.SqlServer/Metadata/Internal/SqlServerKeyExtensions.cs b/src/EFCore.SqlServer/Metadata/Internal/SqlServerKeyExtensions.cs index 13d718a0d18..eb43fd235e1 100644 --- a/src/EFCore.SqlServer/Metadata/Internal/SqlServerKeyExtensions.cs +++ b/src/EFCore.SqlServer/Metadata/Internal/SqlServerKeyExtensions.cs @@ -24,12 +24,11 @@ public static class SqlServerKeyExtensions public static bool AreCompatibleForSqlServer( [NotNull] this IKey key, [NotNull] IKey duplicateKey, - [NotNull] string tableName, - [CanBeNull] string schema, + StoreObjectIdentifier storeObject, bool shouldThrow) { - if (key.IsClustered(tableName, schema) - != duplicateKey.IsClustered(tableName, schema)) + if (key.IsClustered(storeObject) + != duplicateKey.IsClustered(storeObject)) { if (shouldThrow) { @@ -39,8 +38,8 @@ public static bool AreCompatibleForSqlServer( key.DeclaringEntityType.DisplayName(), duplicateKey.Properties.Format(), duplicateKey.DeclaringEntityType.DisplayName(), - tableName, - key.GetName(tableName, schema))); + storeObject.DisplayName(), + key.GetName(storeObject))); } return false; diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index cb473a53d3f..17329677236 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs @@ -168,6 +168,60 @@ public virtual void Detects_duplicate_table_names_when_no_key() model); } + [ConditionalFact] + public virtual void Detects_duplicate_view_names_without_identifying_relationship() + { + var model = CreateConventionlessModelBuilder().Model; + + var entityA = model.AddEntityType(typeof(A)); + SetPrimaryKey(entityA); + AddProperties(entityA); + + var entityB = model.AddEntityType(typeof(B)); + SetPrimaryKey(entityB); + AddProperties(entityB); + entityB.AddIgnored(nameof(B.A)); + entityB.AddIgnored(nameof(B.AnotherA)); + entityB.AddIgnored(nameof(B.ManyAs)); + + entityA.SetViewName("Table"); + entityA.SetViewSchema("Schema"); + entityB.SetViewName("Table"); + entityB.SetViewSchema("Schema"); + + VerifyError( + RelationalStrings.IncompatibleViewNoRelationship( + "Schema.Table", entityB.DisplayName(), entityA.DisplayName()), + model); + } + + [ConditionalFact] + public virtual void Detects_duplicate_view_names_when_no_key() + { + var model = CreateConventionlessModelBuilder().Model; + + var entityA = model.AddEntityType(typeof(A)); + entityA.AddProperty("Id", typeof(int)); + entityA.IsKeyless = true; + AddProperties(entityA); + + var entityB = model.AddEntityType(typeof(B)); + entityB.AddProperty("Id", typeof(int)); + entityB.IsKeyless = true; + AddProperties(entityB); + entityB.AddIgnored(nameof(B.A)); + entityB.AddIgnored(nameof(B.AnotherA)); + entityB.AddIgnored(nameof(B.ManyAs)); + + entityA.SetViewName("Table"); + entityB.SetViewName("Table"); + + VerifyError( + RelationalStrings.IncompatibleViewNoRelationship( + "Table", entityB.DisplayName(), entityA.DisplayName()), + model); + } + [ConditionalFact] public virtual void Passes_for_duplicate_table_names_in_different_schema() { @@ -1256,6 +1310,30 @@ public virtual void Detects_linking_relationship_on_derived_type_in_TPT() modelBuilder.Model); } + [ConditionalFact] + public virtual void Detects_linking_relationship_on_derived_type_in_TPT_views() + { + var modelBuilder = CreateConventionalModelBuilder(); + + modelBuilder.Entity() + .Ignore(a => a.FavoritePerson) + .ToView("Animal"); + + modelBuilder.Entity( + x => + { + x.ToView("Cat"); + x.HasOne(c => c.FavoritePerson).WithOne().HasForeignKey(c => c.Id); + }); + + modelBuilder.Entity().ToView("Cat"); + + VerifyError( + RelationalStrings.IncompatibleViewDerivedRelationship( + "Cat", "Cat", "Person"), + modelBuilder.Model); + } + [ConditionalFact] public virtual void Passes_for_valid_table_overrides() {