From 08474d6d13196ea1beb85acacca66eaf6be05355 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Thu, 13 Jan 2022 22:12:38 -0800 Subject: [PATCH] Add metadata support for TPC Part of #3170 --- .../Internal/SnapshotModelProcessor.cs | 4 +- .../Diagnostics/RelationalEventId.cs | 30 + .../Diagnostics/RelationalLoggerExtensions.cs | 96 +++ .../RelationalLoggingDefinitions.cs | 18 + .../RelationalEntityTypeBuilderExtensions.cs | 73 +++ .../RelationalEntityTypeExtensions.cs | 92 ++- .../RelationalForeignKeyExtensions.cs | 12 + .../RelationalPropertyExtensions.cs | 38 +- .../RelationalModelValidator.cs | 95 ++- .../EntityTypeHierarchyMappingConvention.cs | 54 +- .../Metadata/Internal/CheckConstraint.cs | 48 +- .../Metadata/Internal/RelationalModel.cs | 313 ++++----- .../Metadata/RelationalAnnotationNames.cs | 20 + .../Properties/RelationalStrings.Designer.cs | 95 ++- .../Properties/RelationalStrings.resx | 27 +- .../Design/CSharpMigrationsGeneratorTest.cs | 8 + .../Design/SnapshotModelProcessorTest.cs | 6 +- .../CSharpRuntimeModelCodeGeneratorTest.cs | 446 ++++++++++++- .../RelationalModelValidatorTest.cs | 312 ++++++++- .../Metadata/RelationalModelTest.cs | 604 +++++++++++++----- .../RelationalTestModelBuilderExtensions.cs | 51 ++ .../SqlServerModelBuilderGenericTest.cs | 84 ++- 22 files changed, 2139 insertions(+), 387 deletions(-) diff --git a/src/EFCore.Design/Migrations/Internal/SnapshotModelProcessor.cs b/src/EFCore.Design/Migrations/Internal/SnapshotModelProcessor.cs index d787e6620ea..c50af70cfec 100644 --- a/src/EFCore.Design/Migrations/Internal/SnapshotModelProcessor.cs +++ b/src/EFCore.Design/Migrations/Internal/SnapshotModelProcessor.cs @@ -34,7 +34,9 @@ public SnapshotModelProcessor( typeof(RelationalAnnotationNames) .GetRuntimeFields() .Where(p => p.Name != nameof(RelationalAnnotationNames.Prefix)) - .Select(p => ((string)p.GetValue(null)!)[(RelationalAnnotationNames.Prefix.Length - 1)..])); + .Select(p => (string)p.GetValue(null)!) + .Where(v => v.IndexOf(':') > 0) + .Select(v => v[(RelationalAnnotationNames.Prefix.Length - 1)..])); _modelRuntimeInitializer = modelRuntimeInitializer; } diff --git a/src/EFCore.Relational/Diagnostics/RelationalEventId.cs b/src/EFCore.Relational/Diagnostics/RelationalEventId.cs index e231a0b98aa..68c19dcd735 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalEventId.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalEventId.cs @@ -89,6 +89,8 @@ private enum Id ForeignKeyPropertiesMappedToUnrelatedTables, OptionalDependentWithoutIdentifyingPropertyWarning, DuplicateColumnOrders, + ForeignKeyTPCPrincipalWarning, + TpcStoreGeneratedIdentityWarning, // Update events BatchReadyForExecution = CoreEventId.RelationalBaseId + 700, @@ -739,6 +741,34 @@ private static EventId MakeValidationId(Id id) public static readonly EventId ForeignKeyPropertiesMappedToUnrelatedTables = MakeValidationId(Id.ForeignKeyPropertiesMappedToUnrelatedTables); + /// + /// A foreign key specifies properties which don't map to the related tables. + /// + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId ForeignKeyTPCPrincipalWarning = + MakeValidationId(Id.ForeignKeyTPCPrincipalWarning); + + /// + /// The PK is using store-generated values in TPC. + /// + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId TpcStoreGeneratedIdentityWarning = + MakeValidationId(Id.TpcStoreGeneratedIdentityWarning); + /// /// The entity does not have any property with a non-default value to identify whether the entity exists. /// diff --git a/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs b/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs index 34e61294691..47fcbfbe492 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs @@ -2831,6 +2831,102 @@ private static string ForeignKeyPropertiesMappedToUnrelatedTables(EventDefinitio p.ForeignKey.PrincipalEntityType.GetSchemaQualifiedTableName())); } + /// + /// Logs the event. + /// + /// The diagnostics logger to use. + /// The foreign key. + public static void ForeignKeyTPCPrincipal( + this IDiagnosticsLogger diagnostics, + IForeignKey foreignKey) + { + var definition = RelationalResources.LogForeignKeyTPCPrincipal(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log( + diagnostics, + l => l.Log( + definition.Level, + definition.EventId, + definition.MessageFormat, + foreignKey.Properties.Format(), + foreignKey.DeclaringEntityType.DisplayName(), + foreignKey.PrincipalEntityType.DisplayName(), + foreignKey.PrincipalEntityType.DisplayName(), + foreignKey.PrincipalEntityType.GetSchemaQualifiedTableName()!, + foreignKey.DeclaringEntityType.DisplayName(), + foreignKey.PrincipalEntityType.DisplayName())); + } + + if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = new ForeignKeyEventData( + definition, + ForeignKeyTPCPrincipal, + foreignKey); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + } + } + + private static string ForeignKeyTPCPrincipal(EventDefinitionBase definition, EventData payload) + { + var d = (FallbackEventDefinition)definition; + var p = (ForeignKeyEventData)payload; + return d.GenerateMessage( + l => l.Log( + d.Level, + d.EventId, + d.MessageFormat, + p.ForeignKey.Properties.Format(), + p.ForeignKey.DeclaringEntityType.DisplayName(), + p.ForeignKey.PrincipalEntityType.DisplayName(), + p.ForeignKey.PrincipalEntityType.GetSchemaQualifiedTableName()!, + p.ForeignKey.PrincipalEntityType.DisplayName(), + p.ForeignKey.DeclaringEntityType.DisplayName(), + p.ForeignKey.PrincipalEntityType.DisplayName())); + } + + /// + /// Logs the event. + /// + /// The diagnostics logger to use. + /// The entity type on which the index is defined. + public static void TpcStoreGeneratedIdentity( + this IDiagnosticsLogger diagnostics, + IProperty property) + { + var definition = RelationalResources.LogTpcStoreGeneratedIdentity(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log( + diagnostics, + property.Name, + property.DeclaringEntityType.DisplayName()); + } + + if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = new PropertyEventData( + definition, + TpcStoreGeneratedIdentity, + property); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + } + } + + private static string TpcStoreGeneratedIdentity(EventDefinitionBase definition, EventData payload) + { + var d = (EventDefinition)definition; + var p = (PropertyEventData)payload; + return d.GenerateMessage( + p.Property.Name, + p.Property.DeclaringEntityType.DisplayName()); + } + /// /// Logs the event. /// diff --git a/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs b/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs index f39c7e15fc0..b2e97e7afb1 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs @@ -484,6 +484,24 @@ public abstract class RelationalLoggingDefinitions : LoggingDefinitions [EntityFrameworkInternal] public EventDefinitionBase? LogForeignKeyPropertiesMappedToUnrelatedTables; + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + public EventDefinitionBase? LogForeignKeyTPCPrincipal; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + public EventDefinitionBase? LogTpcStoreGeneratedIdentity; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs index 98000b2490b..dae7f560de1 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs @@ -15,6 +15,79 @@ namespace Microsoft.EntityFrameworkCore; /// public static class RelationalEntityTypeBuilderExtensions { + /// + /// Configures TPC as the mapping strategy for the derived types. Each type will be mapped to a different database object. + /// All properties will be mapped to columns on the corresponding object. + /// + /// The builder for the entity type being configured. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder UseTpcMappingStrategy(this EntityTypeBuilder entityTypeBuilder) + { + entityTypeBuilder.Metadata.SetMappingStrategy(RelationalAnnotationNames.TpcMappingStrategy); + + return entityTypeBuilder; + } + + /// + /// Configures TPH as the mapping strategy for the derived types. All types will be mapped to the same database object. + /// This is the default mapping strategy. + /// + /// The builder for the entity type being configured. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder UseTphMappingStrategy(this EntityTypeBuilder entityTypeBuilder) + { + entityTypeBuilder.Metadata.SetMappingStrategy(RelationalAnnotationNames.TphMappingStrategy); + + return entityTypeBuilder; + } + + /// + /// Configures TPT as the mapping strategy for the derived types. Each type will be mapped to a different database object. + /// Only the declared properties will be mapped to columns on the corresponding object. + /// + /// The builder for the entity type being configured. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder UseTptMappingStrategy(this EntityTypeBuilder entityTypeBuilder) + { + entityTypeBuilder.Metadata.SetMappingStrategy(RelationalAnnotationNames.TptMappingStrategy); + + return entityTypeBuilder; + } + + /// + /// Configures TPC as the mapping strategy for the derived types. Each type will be mapped to a different database object. + /// All properties will be mapped to columns on the corresponding object. + /// + /// The builder for the entity type being configured. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder UseTpcMappingStrategy(this EntityTypeBuilder entityTypeBuilder) + where TEntity : class + => (EntityTypeBuilder)((EntityTypeBuilder)entityTypeBuilder).UseTpcMappingStrategy(); + + /// + /// Configures TPH as the mapping strategy for the derived types. All types will be mapped to the same database object. + /// This is the default mapping strategy. + /// + /// The builder for the entity type being configured. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder UseTphMappingStrategy(this EntityTypeBuilder entityTypeBuilder) + where TEntity : class + => (EntityTypeBuilder)((EntityTypeBuilder)entityTypeBuilder).UseTphMappingStrategy(); + + /// + /// Configures TPT as the mapping strategy for the derived types. Each type will be mapped to a different database object. + /// Only the declared properties will be mapped to columns on the corresponding object. + /// + /// + /// See Database migrations for more information and examples. + /// + /// The entity type being configured. + /// The builder for the entity type being configured. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder UseTptMappingStrategy(this EntityTypeBuilder entityTypeBuilder) + where TEntity : class + => (EntityTypeBuilder)((EntityTypeBuilder)entityTypeBuilder).UseTptMappingStrategy(); + /// /// Configures the table that the entity type maps to when targeting a relational database. /// diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs index fea3d7d740d..e96f5136138 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs @@ -34,17 +34,12 @@ public static class RelationalEntityTypeExtensions return (string?)nameAnnotation.Value; } - if (entityType.BaseType != null) - { - return entityType.GetRootType().GetTableName(); - } - return ((entityType as IConventionEntityType)?.GetViewNameConfigurationSource() == null) - && ((entityType as IConventionEntityType)?.GetFunctionNameConfigurationSource() == null) + && (entityType as IConventionEntityType)?.GetFunctionNameConfigurationSource() == null #pragma warning disable CS0618 // Type or member is obsolete - && ((entityType as IConventionEntityType)?.GetDefiningQueryConfigurationSource() == null) + && (entityType as IConventionEntityType)?.GetDefiningQueryConfigurationSource() == null #pragma warning restore CS0618 // Type or member is obsolete - && ((entityType as IConventionEntityType)?.GetSqlQueryConfigurationSource() == null) + && (entityType as IConventionEntityType)?.GetSqlQueryConfigurationSource() == null ? GetDefaultTableName(entityType) : null; } @@ -57,6 +52,12 @@ public static class RelationalEntityTypeExtensions /// The default name of the table to which the entity type would be mapped. public static string? GetDefaultTableName(this IReadOnlyEntityType entityType, bool truncate = true) { + if (entityType.GetDiscriminatorPropertyName() != null + && entityType.BaseType != null) + { + return entityType.GetRootType().GetTableName(); + } + var ownership = entityType.FindOwnership(); if (ownership != null && ownership.IsUnique) @@ -77,6 +78,12 @@ public static class RelationalEntityTypeExtensions : $"{ownership.PrincipalToDependent.Name}_{name}"; } + if (entityType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy + && !entityType.ClrType.IsInstantiable()) + { + return null; + } + return truncate ? Uniquifier.Truncate(name, entityType.Model.GetMaxIdentifierLength()) : name; @@ -273,15 +280,10 @@ public static IEnumerable GetTableMappings(this IEntityType entit /// The name of the view to which the entity type is mapped. public static string? GetViewName(this IReadOnlyEntityType entityType) { - var nameAnnotation = (string?)entityType[RelationalAnnotationNames.ViewName]; + var nameAnnotation = entityType.FindAnnotation(RelationalAnnotationNames.ViewName); if (nameAnnotation != null) { - return nameAnnotation; - } - - if (entityType.BaseType != null) - { - return entityType.GetRootType().GetViewName(); + return (string?)nameAnnotation.Value; } return ((entityType as IConventionEntityType)?.GetFunctionNameConfigurationSource() == null) @@ -300,6 +302,12 @@ public static IEnumerable GetTableMappings(this IEntityType entit /// The default name of the table to which the entity type would be mapped. public static string? GetDefaultViewName(this IReadOnlyEntityType entityType) { + if (entityType.GetDiscriminatorPropertyName() != null + && entityType.BaseType != null) + { + return entityType.GetRootType().GetViewName(); + } + var ownership = entityType.FindOwnership(); return ownership != null && ownership.IsUnique @@ -957,4 +965,58 @@ public static void SetIsTableExcludedFromMigrations(this IMutableEntityType enti this IConventionEntityType entityType) => entityType.FindAnnotation(RelationalAnnotationNames.IsTableExcludedFromMigrations) ?.GetConfigurationSource(); + + /// + /// Gets a value indicating whether the inherited properties are mapped to the same table as the derived ones. + /// + /// The entity type. + /// A value indicating whether the inherited properties are mapped to the same table as the derived ones. + public static string? GetMappingStrategy(this IReadOnlyEntityType entityType) + { + var inherited = (string?)entityType[RelationalAnnotationNames.MappingStrategy]; + if (inherited != null) + { + return inherited; + } + + if (entityType.BaseType != null) + { + return entityType.GetRootType().GetMappingStrategy(); + } + + return null; + } + + /// + /// Sets a value indicating whether the inherited properties are mapped to the same table as the derived ones. + /// + /// The entity type. + /// A value indicating whether the inherited properties are mapped to the same table as the derived ones. + public static void SetMappingStrategy(this IMutableEntityType entityType, string? strategy) + => entityType.SetOrRemoveAnnotation(RelationalAnnotationNames.MappingStrategy, strategy); + + /// + /// Sets a value indicating whether the inherited properties are mapped to the same table as the derived ones. + /// + /// The entity type. + /// A value indicating whether the inherited properties are mapped to the same table as the derived ones. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static string? SetMappingStrategy( + this IConventionEntityType entityType, + string? strategy, + bool fromDataAnnotation = false) + => (string?)entityType.SetOrRemoveAnnotation( + RelationalAnnotationNames.MappingStrategy, strategy, fromDataAnnotation) + ?.Value; + + /// + /// Gets the for . + /// + /// The entity type to find configuration source for. + /// The for . + public static ConfigurationSource? GetMappingStrategyConfigurationSource( + this IConventionEntityType entityType) + => entityType.FindAnnotation(RelationalAnnotationNames.MappingStrategy) + ?.GetConfigurationSource(); } diff --git a/src/EFCore.Relational/Extensions/RelationalForeignKeyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalForeignKeyExtensions.cs index efeff92fa22..950deee7165 100644 --- a/src/EFCore.Relational/Extensions/RelationalForeignKeyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalForeignKeyExtensions.cs @@ -72,6 +72,12 @@ public static class RelationalForeignKeyExtensions return null; } + if (foreignKey.PrincipalEntityType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy + && foreignKey.PrincipalEntityType.GetDerivedTypes().Any(et => StoreObjectIdentifier.Create(et, StoreObjectType.Table) != null)) + { + return null; + } + var name = new StringBuilder() .Append("FK_") .Append(tableName) @@ -150,6 +156,12 @@ public static class RelationalForeignKeyExtensions return rootForeignKey.GetConstraintName(storeObject, principalStoreObject); } + if (foreignKey.PrincipalEntityType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy + && foreignKey.PrincipalEntityType.GetDerivedTypes().Any(et => StoreObjectIdentifier.Create(et, StoreObjectType.Table) != null)) + { + return null; + } + var baseName = new StringBuilder() .Append("FK_") .Append(storeObject.Name) diff --git a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs index 06eba790d14..0ccea516d6c 100644 --- a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs @@ -58,9 +58,42 @@ public static string GetColumnBaseName(this IReadOnlyProperty property) return null; } } - else if (StoreObjectIdentifier.Create(property.DeclaringEntityType, storeObject.StoreObjectType) != storeObject) + else if (property.DeclaringEntityType.GetMappingStrategy() != RelationalAnnotationNames.TpcMappingStrategy) { - return null; + var declaringStoreObject = StoreObjectIdentifier.Create(property.DeclaringEntityType, storeObject.StoreObjectType); + if (declaringStoreObject == null) + { + var tableFound = false; + var queue = new Queue(); + queue.Enqueue(property.DeclaringEntityType); + while (queue.Count > 0 && !tableFound) + { + foreach (var containingType in queue.Dequeue().GetDirectlyDerivedTypes()) + { + declaringStoreObject = StoreObjectIdentifier.Create(containingType, storeObject.StoreObjectType); + if (declaringStoreObject == null) + { + queue.Enqueue(containingType); + continue; + } + + if (declaringStoreObject == storeObject) + { + tableFound = true; + break; + } + } + } + + if (!tableFound) + { + return null; + } + } + else if (declaringStoreObject != storeObject) + { + return null; + } } } @@ -1278,7 +1311,6 @@ public static RelationalTypeMapping GetRelationalTypeMapping(this IReadOnlyPrope private static IReadOnlyProperty? FindSharedObjectRootProperty(IReadOnlyProperty property, in StoreObjectIdentifier storeObject) { var column = property.GetColumnName(storeObject); - if (column == null) { throw new InvalidOperationException( diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index 59c452f597d..fca8b78e1f3 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs @@ -1051,6 +1051,11 @@ protected virtual void ValidateSharedForeignKeysCompatibility( var foreignKeyName = foreignKey.GetConstraintName(storeObject, principalTable.Value); if (foreignKeyName == null) { + if (foreignKey.PrincipalEntityType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy) + { + logger.ForeignKeyTPCPrincipal(foreignKey); + } + var derivedTables = foreignKey.DeclaringEntityType.GetDerivedTypes() .Select(t => StoreObjectIdentifier.Create(t, StoreObjectType.Table)) .Where(t => t != null); @@ -1240,31 +1245,99 @@ protected override void ValidateInheritanceMapping( IModel model, IDiagnosticsLogger logger) { - foreach (var rootEntityType in model.GetEntityTypes()) + foreach (var entityType in model.GetEntityTypes()) { - if (rootEntityType.BaseType != null) + var mappingStrategy = (string?)entityType[RelationalAnnotationNames.MappingStrategy]; + if (mappingStrategy != null) + { + ValidateMappingStrategy(mappingStrategy, entityType); + var storeObject = entityType.GetSchemaQualifiedTableName() + ?? entityType.GetSchemaQualifiedViewName() + ?? entityType.GetFunctionName(); + if (mappingStrategy == RelationalAnnotationNames.TpcMappingStrategy + && !entityType.ClrType.IsInstantiable() + && storeObject != null) + { + throw new InvalidOperationException( + RelationalStrings.AbstractTPC(entityType.DisplayName(), storeObject)); + } + } + + if (entityType.BaseType != null) + { + if (mappingStrategy != null + && mappingStrategy != entityType.BaseType.GetMappingStrategy()) + { + throw new InvalidOperationException( + RelationalStrings.DerivedStrategy(entityType.DisplayName(), mappingStrategy)); + } + + continue; + } + + if (!entityType.GetDirectlyDerivedTypes().Any()) { continue; } // Hierarchy mapping strategy must be the same across all types of mappings - var isTph = rootEntityType.FindPrimaryKey() == null - || rootEntityType.FindDiscriminatorProperty() != null; - if (isTph) + if (entityType.FindDiscriminatorProperty() != null) { - ValidateTPHMapping(rootEntityType, forTables: false); - ValidateTPHMapping(rootEntityType, forTables: true); - ValidateDiscriminatorValues(rootEntityType); + if (mappingStrategy != null + && mappingStrategy != RelationalAnnotationNames.TphMappingStrategy) + { + throw new InvalidOperationException( + RelationalStrings.NonTphMappingStrategy(mappingStrategy, entityType.DisplayName())); + } + + ValidateTPHMapping(entityType, forTables: false); + ValidateTPHMapping(entityType, forTables: true); + ValidateDiscriminatorValues(entityType); } else { - ValidateTPTMapping(rootEntityType, forTables: false); - ValidateTPTMapping(rootEntityType, forTables: true); + var pk = entityType.FindPrimaryKey(); + if (mappingStrategy == RelationalAnnotationNames.TpcMappingStrategy) + { + var storeGeneratedProperty = pk?.Properties.FirstOrDefault(p => (p.ValueGenerated & ValueGenerated.OnAdd) != 0); + if (storeGeneratedProperty != null + && entityType.GetTableName() != null) + { + logger.TpcStoreGeneratedIdentity(storeGeneratedProperty); + } + } + else if (pk == null) + { + throw new InvalidOperationException( + RelationalStrings.KeylessMappingStrategy(mappingStrategy ?? RelationalAnnotationNames.TptMappingStrategy, entityType.DisplayName())); + } + + ValidateNonTPHMapping(entityType, forTables: false); + ValidateNonTPHMapping(entityType, forTables: true); } } } - private static void ValidateTPTMapping(IEntityType rootEntityType, bool forTables) + /// + /// Validates that the given mapping strategy is supported + /// + /// The mapping strategy. + /// The entity type. + protected virtual void ValidateMappingStrategy(string? mappingStrategy, IEntityType entityType) + { + switch (mappingStrategy) + { + case RelationalAnnotationNames.TphMappingStrategy: + case RelationalAnnotationNames.TpcMappingStrategy: + case RelationalAnnotationNames.TptMappingStrategy: + break; + default: + throw new InvalidOperationException(RelationalStrings.InvalidMappingStrategy( + mappingStrategy, entityType.DisplayName())); + }; + } + + private static void ValidateNonTPHMapping(IEntityType rootEntityType, bool forTables) { var derivedTypes = new Dictionary<(string, string?), IEntityType>(); foreach (var entityType in rootEntityType.GetDerivedTypesInclusive()) diff --git a/src/EFCore.Relational/Metadata/Conventions/EntityTypeHierarchyMappingConvention.cs b/src/EFCore.Relational/Metadata/Conventions/EntityTypeHierarchyMappingConvention.cs index f32bbd979e7..85530ab9ce7 100644 --- a/src/EFCore.Relational/Metadata/Conventions/EntityTypeHierarchyMappingConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/EntityTypeHierarchyMappingConvention.cs @@ -49,29 +49,57 @@ public virtual void ProcessModelFinalizing( continue; } + var mappingStrategy = entityType.GetMappingStrategy(); + if (mappingStrategy == RelationalAnnotationNames.TpcMappingStrategy) + { + nonTphRoots.Add(entityType.GetRootType()); + continue; + } + var tableName = entityType.GetTableName(); - var schema = entityType.GetSchema(); - if (tableName != null - && (tableName != entityType.BaseType.GetTableName() - || schema != entityType.BaseType.GetSchema())) + if (tableName != null) { - var pk = entityType.FindPrimaryKey(); - if (pk != null - && !entityType.FindDeclaredForeignKeys(pk.Properties) - .Any(fk => fk.PrincipalKey.IsPrimaryKey() && fk.PrincipalEntityType.IsAssignableFrom(entityType))) + if (mappingStrategy == null) { - entityType.Builder.HasRelationship(entityType.BaseType, pk.Properties, pk)? - .IsUnique(true); + if (tableName != entityType.BaseType.GetTableName() + || entityType.GetSchema() != entityType.BaseType.GetSchema()) + { + mappingStrategy = RelationalAnnotationNames.TptMappingStrategy; + } } - nonTphRoots.Add(entityType.GetRootType()); + if (mappingStrategy == RelationalAnnotationNames.TptMappingStrategy) + { + var pk = entityType.FindPrimaryKey(); + if (pk != null + && !entityType.FindDeclaredForeignKeys(pk.Properties) + .Any(fk => fk.PrincipalKey.IsPrimaryKey() + && fk.PrincipalEntityType.IsAssignableFrom(entityType) + && fk.PrincipalEntityType != entityType)) + { + var closestMappedType = entityType.BaseType; + while (closestMappedType != null + && closestMappedType.GetTableName() == null) + { + closestMappedType = closestMappedType.BaseType; + } + + if (closestMappedType != null) + { + entityType.Builder.HasRelationship(closestMappedType, pk.Properties, pk)? + .IsUnique(true); + } + } + + nonTphRoots.Add(entityType.GetRootType()); + continue; + } } var viewName = entityType.GetViewName(); - var viewSchema = entityType.GetViewSchema(); if (viewName != null && (viewName != entityType.BaseType.GetViewName() - || viewSchema != entityType.BaseType.GetViewSchema())) + || entityType.GetViewSchema() != entityType.BaseType.GetViewSchema())) { nonTphRoots.Add(entityType.GetRootType()); } diff --git a/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs b/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs index f8655df492d..3d04662e534 100644 --- a/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs +++ b/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs @@ -296,15 +296,55 @@ public virtual string? Name return null; } - foreach (var containingType in EntityType.GetDerivedTypesInclusive()) + if (EntityType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy) { - if (StoreObjectIdentifier.Create(containingType, storeObject.StoreObjectType) == storeObject) + foreach (var containingType in EntityType.GetDerivedTypesInclusive()) { - return _name ?? ((IReadOnlyCheckConstraint)this).GetDefaultName(storeObject); + if (StoreObjectIdentifier.Create(containingType, storeObject.StoreObjectType) == storeObject) + { + return _name ?? ((IReadOnlyCheckConstraint)this).GetDefaultName(storeObject); + } } + + return null; } - return null; + var declaringStoreObject = StoreObjectIdentifier.Create(EntityType, storeObject.StoreObjectType); + if (declaringStoreObject == null) + { + var tableFound = false; + var queue = new Queue(); + queue.Enqueue(EntityType); + while (queue.Count > 0 && !tableFound) + { + foreach (var containingType in queue.Dequeue().GetDirectlyDerivedTypes()) + { + declaringStoreObject = StoreObjectIdentifier.Create(containingType, storeObject.StoreObjectType); + if (declaringStoreObject == null) + { + queue.Enqueue(containingType); + continue; + } + + if (declaringStoreObject == storeObject) + { + tableFound = true; + break; + } + } + } + + if (!tableFound) + { + return null; + } + } + else if (declaringStoreObject != storeObject) + { + return null; + } + + return _name ?? ((IReadOnlyCheckConstraint)this).GetDefaultName(storeObject); } /// diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs index 1c6b8dd5d43..ef68653f236 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs @@ -245,65 +245,80 @@ public static IRelationalModel Create( private static void AddDefaultMappings(RelationalModel databaseModel, IEntityType entityType) { - var rootType = entityType.GetRootType(); - var name = rootType.Name; - if (!databaseModel.DefaultTables.TryGetValue(name, out var defaultTable)) - { - defaultTable = new TableBase(name, null, databaseModel); - databaseModel.DefaultTables.Add(name, defaultTable); - } - - var tableMapping = new TableMappingBase(entityType, defaultTable, includesDerivedTypes: true) - { - IsSharedTablePrincipal = true, IsSplitEntityTypePrincipal = true - }; + var mappedType = entityType; + Check.DebugAssert(entityType.FindRuntimeAnnotationValue(RelationalAnnotationNames.DefaultMappings) == null, "not null"); + var tableMappings = new List(); + entityType.AddRuntimeAnnotation(RelationalAnnotationNames.DefaultMappings, tableMappings); - foreach (var property in entityType.GetProperties()) + var isTpc = entityType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy; + var isTph = entityType.FindDiscriminatorProperty() != null; + while (mappedType != null) { - var columnName = property.GetColumnBaseName(); - if (columnName == null) + var mappedTableName = isTph ? entityType.GetRootType().Name : mappedType.Name; + if (!databaseModel.DefaultTables.TryGetValue(mappedTableName, out var defaultTable)) { - continue; + defaultTable = new TableBase(mappedTableName, null, databaseModel); + databaseModel.DefaultTables.Add(mappedTableName, defaultTable); } - var column = (ColumnBase?)defaultTable.FindColumn(columnName); - if (column == null) + var tableMapping = new TableMappingBase(entityType, defaultTable, includesDerivedTypes: !isTpc && mappedType == entityType) { - column = new ColumnBase(columnName, property.GetColumnType(), defaultTable); - column.IsNullable = property.IsColumnNullable(); - defaultTable.Columns.Add(columnName, column); - } - else if (!property.IsColumnNullable()) + // Table splitting is not supported for default mapping + IsSharedTablePrincipal = true, + IsSplitEntityTypePrincipal = true + }; + + foreach (var property in entityType.GetProperties()) { - column.IsNullable = false; - } + var columnName = property.IsPrimaryKey() || isTpc || isTph || property.DeclaringEntityType == mappedType + ? property.GetColumnBaseName() + : null; + if (columnName == null) + { + continue; + } - var columnMapping = new ColumnMappingBase(property, column, tableMapping); - tableMapping.ColumnMappings.Add(columnMapping); - column.PropertyMappings.Add(columnMapping); + var column = (ColumnBase?)defaultTable.FindColumn(columnName); + if (column == null) + { + column = new ColumnBase(columnName, property.GetColumnType(), defaultTable) + { + IsNullable = property.IsColumnNullable() + }; + defaultTable.Columns.Add(columnName, column); + } + else if (!property.IsColumnNullable()) + { + column.IsNullable = false; + } - if (property.FindRuntimeAnnotationValue(RelationalAnnotationNames.DefaultColumnMappings) - is not SortedSet columnMappings) - { - columnMappings = new SortedSet(ColumnMappingBaseComparer.Instance); - property.AddRuntimeAnnotation(RelationalAnnotationNames.DefaultColumnMappings, columnMappings); + var columnMapping = new ColumnMappingBase(property, column, tableMapping); + tableMapping.ColumnMappings.Add(columnMapping); + column.PropertyMappings.Add(columnMapping); + + if (property.FindRuntimeAnnotationValue(RelationalAnnotationNames.DefaultColumnMappings) + is not SortedSet columnMappings) + { + columnMappings = new SortedSet(ColumnMappingBaseComparer.Instance); + property.AddRuntimeAnnotation(RelationalAnnotationNames.DefaultColumnMappings, columnMappings); + } + + columnMappings.Add(columnMapping); } - columnMappings.Add(columnMapping); - } + if (tableMapping.ColumnMappings.Count != 0 + || tableMappings.Count == 0) + { + tableMappings.Add(tableMapping); + defaultTable.EntityTypeMappings.Add(tableMapping); + } - if (entityType.FindRuntimeAnnotationValue(RelationalAnnotationNames.DefaultMappings) - is not List tableMappings) - { - tableMappings = new List(); - entityType.AddRuntimeAnnotation(RelationalAnnotationNames.DefaultMappings, tableMappings); - } + if (isTpc || isTph) + { + break; + } - if (tableMapping.ColumnMappings.Count != 0 - || tableMappings.Count == 0) - { - tableMappings.Add(tableMapping); - defaultTable.EntityTypeMappings.Add(tableMapping); + mappedType = mappedType.BaseType; } tableMappings.Reverse(); @@ -312,89 +327,98 @@ private static void AddDefaultMappings(RelationalModel databaseModel, IEntityTyp private static void AddTables(RelationalModel databaseModel, IEntityType entityType) { var tableName = entityType.GetTableName(); - if (tableName != null) + if (tableName == null) { - var schema = entityType.GetSchema(); - var mappedType = entityType; - List? tableMappings = null; - while (mappedType != null) - { - var mappedTableName = mappedType.GetTableName(); - var mappedSchema = mappedType.GetSchema(); + return; + } - if (mappedTableName == null - || (mappedTableName == tableName - && mappedSchema == schema - && mappedType != entityType)) + var mappedType = entityType; + + Check.DebugAssert(entityType.FindRuntimeAnnotationValue(RelationalAnnotationNames.TableMappings) == null, "not null"); + var tableMappings = new List(); + entityType.SetRuntimeAnnotation(RelationalAnnotationNames.TableMappings, tableMappings); + + var isTpc = entityType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy; + var isTph = entityType.FindDiscriminatorProperty() != null; + while (mappedType != null) + { + var mappedTableName = mappedType.GetTableName(); + var mappedSchema = mappedType.GetSchema(); + + if (mappedTableName == null) + { + if (isTpc) { break; } - var mappedTable = StoreObjectIdentifier.Table(mappedTableName, mappedSchema); - if (!databaseModel.Tables.TryGetValue((mappedTableName, mappedSchema), out var table)) + mappedType = mappedType.BaseType; + continue; + } + + if (!databaseModel.Tables.TryGetValue((mappedTableName, mappedSchema), out var table)) + { + table = new Table(mappedTableName, mappedSchema, databaseModel); + databaseModel.Tables.Add((mappedTableName, mappedSchema), table); + } + + var mappedTable = StoreObjectIdentifier.Table(mappedTableName, mappedSchema); + var tableMapping = new TableMapping(entityType, table, includesDerivedTypes: !isTpc && mappedType == entityType) + { + IsSplitEntityTypePrincipal = true + }; + foreach (var property in mappedType.GetProperties()) + { + var columnName = property.GetColumnName(mappedTable); + if (columnName == null) { - table = new Table(mappedTableName, mappedSchema, databaseModel); - databaseModel.Tables.Add((mappedTableName, mappedSchema), table); + continue; } - var tableMapping = new TableMapping(entityType, table, includesDerivedTypes: mappedType == entityType) - { - IsSplitEntityTypePrincipal = true - }; - foreach (var property in mappedType.GetProperties()) + var column = (Column?)table.FindColumn(columnName); + if (column == null) { - var columnName = property.GetColumnName(mappedTable); - if (columnName == null) + column = new(columnName, property.GetColumnType(mappedTable), table) { - continue; - } - - var column = (Column?)table.FindColumn(columnName); - if (column == null) - { - column = new Column(columnName, property.GetColumnType(mappedTable), table); - column.IsNullable = property.IsColumnNullable(mappedTable); - table.Columns.Add(columnName, column); - } - else if (!property.IsColumnNullable(mappedTable)) - { - column.IsNullable = false; - } - - var columnMapping = new ColumnMapping(property, column, tableMapping); - tableMapping.ColumnMappings.Add(columnMapping); - column.PropertyMappings.Add(columnMapping); - - if (property.FindRuntimeAnnotationValue(RelationalAnnotationNames.TableColumnMappings) - is not SortedSet columnMappings) - { - columnMappings = new SortedSet(ColumnMappingBaseComparer.Instance); - property.AddRuntimeAnnotation(RelationalAnnotationNames.TableColumnMappings, columnMappings); - } - - columnMappings.Add(columnMapping); + IsNullable = property.IsColumnNullable(mappedTable) + }; + table.Columns.Add(columnName, column); } - - mappedType = mappedType.BaseType; - - tableMappings = entityType.FindRuntimeAnnotationValue(RelationalAnnotationNames.TableMappings) - as List; - if (tableMappings == null) + else if (!property.IsColumnNullable(mappedTable)) { - tableMappings = new List(); - entityType.AddRuntimeAnnotation(RelationalAnnotationNames.TableMappings, tableMappings); + column.IsNullable = false; } - if (tableMapping.ColumnMappings.Count != 0 - || tableMappings.Count == 0) + var columnMapping = new ColumnMapping(property, column, tableMapping); + tableMapping.ColumnMappings.Add(columnMapping); + column.PropertyMappings.Add(columnMapping); + + if (property.FindRuntimeAnnotationValue(RelationalAnnotationNames.TableColumnMappings) + is not SortedSet columnMappings) { - tableMappings.Add(tableMapping); - table.EntityTypeMappings.Add(tableMapping); + columnMappings = new SortedSet(ColumnMappingBaseComparer.Instance); + property.AddRuntimeAnnotation(RelationalAnnotationNames.TableColumnMappings, columnMappings); } + + columnMappings.Add(columnMapping); + } + + if (tableMapping.ColumnMappings.Count != 0 + || tableMappings.Count == 0) + { + tableMappings.Add(tableMapping); + table.EntityTypeMappings.Add(tableMapping); + } + + if (isTpc || isTph) + { + break; } - tableMappings?.Reverse(); + mappedType = mappedType.BaseType; } + + tableMappings.Reverse(); } private static void AddViews(RelationalModel databaseModel, IEntityType entityType) @@ -405,20 +429,28 @@ private static void AddViews(RelationalModel databaseModel, IEntityType entityTy return; } - var schema = entityType.GetViewSchema(); - List? viewMappings = null; var mappedType = entityType; + + Check.DebugAssert(entityType.FindRuntimeAnnotationValue(RelationalAnnotationNames.ViewMappings) == null, "not null"); + var viewMappings = new List(); + entityType.SetRuntimeAnnotation(RelationalAnnotationNames.ViewMappings, viewMappings); + + var isTpc = entityType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy; + var isTph = entityType.FindDiscriminatorProperty() != null; while (mappedType != null) { var mappedViewName = mappedType.GetViewName(); var mappedSchema = mappedType.GetViewSchema(); - if (mappedViewName == null - || (mappedViewName == viewName - && mappedSchema == schema - && mappedType != entityType)) + if (mappedViewName == null) { - break; + if (isTpc) + { + break; + } + + mappedType = mappedType.BaseType; + continue; } if (!databaseModel.Views.TryGetValue((mappedViewName, mappedSchema), out var view)) @@ -428,7 +460,7 @@ private static void AddViews(RelationalModel databaseModel, IEntityType entityTy } var mappedView = StoreObjectIdentifier.View(mappedViewName, mappedSchema); - var viewMapping = new ViewMapping(entityType, view, includesDerivedTypes: mappedType == entityType) + var viewMapping = new ViewMapping(entityType, view, includesDerivedTypes: !isTpc && mappedType == entityType) { IsSplitEntityTypePrincipal = true }; @@ -443,8 +475,10 @@ private static void AddViews(RelationalModel databaseModel, IEntityType entityTy var column = (ViewColumn?)view.FindColumn(columnName); if (column == null) { - column = new ViewColumn(columnName, property.GetColumnType(mappedView), view); - column.IsNullable = property.IsColumnNullable(mappedView); + column = new ViewColumn(columnName, property.GetColumnType(mappedView), view) + { + IsNullable = property.IsColumnNullable(mappedView) + }; view.Columns.Add(columnName, column); } else if (!property.IsColumnNullable(mappedView)) @@ -466,24 +500,22 @@ private static void AddViews(RelationalModel databaseModel, IEntityType entityTy columnMappings.Add(columnMapping); } - mappedType = mappedType.BaseType; - - viewMappings = entityType.FindRuntimeAnnotationValue(RelationalAnnotationNames.ViewMappings) as List; - if (viewMappings == null) - { - viewMappings = new List(); - entityType.AddRuntimeAnnotation(RelationalAnnotationNames.ViewMappings, viewMappings); - } - if (viewMapping.ColumnMappings.Count != 0 || viewMappings.Count == 0) { viewMappings.Add(viewMapping); view.EntityTypeMappings.Add(viewMapping); } + + if (isTpc || isTph) + { + break; + } + + mappedType = mappedType.BaseType; } - viewMappings?.Reverse(); + viewMappings.Reverse(); } private static void AddSqlQueries(RelationalModel databaseModel, IEntityType entityType) @@ -770,9 +802,11 @@ private static void PopulateConstraints(Table table, bool designTime) { if (firstPrincipalMapping && !principalMapping.IncludesDerivedTypes - && foreignKey.PrincipalEntityType.GetDirectlyDerivedTypes().Any()) + && foreignKey.PrincipalEntityType.GetDirectlyDerivedTypes().Any(e => e.GetTableMappings().Any())) { // Derived principal entity types are mapped to different tables, so the constraint is not enforceable + // Allow this to be overriden #15854 + // TODO: Log a warning break; } @@ -1059,24 +1093,15 @@ private static void PopulateRowInternalForeignKeys(TableBase table) } } - // Re-add the mapping to update the order - if (mainMapping is TableMapping mainTableMapping) - { - ((Table)mainMapping.Table).EntityTypeMappings.Remove(mainTableMapping); - mainMapping.IsSharedTablePrincipal = true; - ((Table)mainMapping.Table).EntityTypeMappings.Add(mainTableMapping); - } - else if (mainMapping is ViewMapping mainViewMapping) - { - ((View)mainMapping.Table).EntityTypeMappings.Remove(mainViewMapping); - mainMapping.IsSharedTablePrincipal = true; - ((View)mainMapping.Table).EntityTypeMappings.Add(mainViewMapping); - } - Check.DebugAssert( mainMapping is not null, $"{nameof(mainMapping)} is neither a {nameof(TableMapping)} nor a {nameof(ViewMapping)}"); + // Re-add the mapping to update the order + mainMapping.Table.EntityTypeMappings.Remove(mainMapping); + mainMapping.IsSharedTablePrincipal = true; + mainMapping.Table.EntityTypeMappings.Add(mainMapping); + if (referencingInternalForeignKeyMap != null) { table.ReferencingRowInternalForeignKeys = referencingInternalForeignKeyMap; diff --git a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs index 64e4776b670..2e40c172622 100644 --- a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs +++ b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs @@ -149,6 +149,26 @@ public static class RelationalAnnotationNames /// public const string IsTableExcludedFromMigrations = Prefix + "IsTableExcludedFromMigrations"; + /// + /// The name for the annotation determining the mapping strategy for inherited properties. + /// + public const string MappingStrategy = Prefix + "MappingStrategy"; + + /// + /// The value for the annotation corresponding to the TPC mapping strategy. + /// + public const string TpcMappingStrategy = "TPC"; + + /// + /// The value for the annotation corresponding to the TPH mapping strategy. + /// + public const string TphMappingStrategy = "TPH"; + + /// + /// The value for the annotation corresponding to the TPT mapping strategy. + /// + public const string TptMappingStrategy = "TPT"; + /// /// The name for database model annotation. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index a96438e2c9b..422760777e1 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -26,6 +26,14 @@ public static class RelationalStrings private static readonly ResourceManager _resourceManager = new ResourceManager("Microsoft.EntityFrameworkCore.Properties.RelationalStrings", typeof(RelationalStrings).Assembly); + /// + /// The corresponding CLR type for entity type '{entityType}' cannot be instantiated, but the entity type was mapped to '{storeObject}' using the 'TPC' mapping strategy. Only instantiable types should be mapped. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. + /// + public static string AbstractTPC(object? entityType, object? storeObject) + => string.Format( + GetString("AbstractTPC", nameof(entityType), nameof(storeObject)), + entityType, storeObject); + /// /// Unable to deserialize a sequence from model metadata. See inner exception for details. /// @@ -295,6 +303,14 @@ public static string DeleteDataOperationValuesCountMismatch(object? valuesCount, GetString("DeleteDataOperationValuesCountMismatch", nameof(valuesCount), nameof(columnsCount), nameof(table)), valuesCount, columnsCount, table); + /// + /// The derived entity type '{entityType}' was configured with the '{strategy}' mapping strategy. Only the root entity type should be configured with a mapping strategy. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. + /// + public static string DerivedStrategy(object? entityType, object? strategy) + => string.Format( + GetString("DerivedStrategy", nameof(entityType), nameof(strategy)), + entityType, strategy); + /// /// Using 'Distinct' operation on a projection containing a collection is not supported. /// @@ -759,6 +775,14 @@ public static string InvalidMappedSqlQueryDerivedType(object? entityType, object GetString("InvalidMappedSqlQueryDerivedType", nameof(entityType), nameof(baseEntityType)), entityType, baseEntityType); + /// + /// The mapping strategy '{mappingStrategy}' specified on '{entityType}' is not supported. + /// + public static string InvalidMappingStrategy(object? mappingStrategy, object? entityType) + => string.Format( + GetString("InvalidMappingStrategy", nameof(mappingStrategy), nameof(entityType)), + mappingStrategy, entityType); + /// /// The specified 'MaxBatchSize' value '{value}' is not valid. It must be a positive number. /// @@ -775,6 +799,14 @@ public static string InvalidMinBatchSize(object? value) GetString("InvalidMinBatchSize", nameof(value)), value); + /// + /// The mapping strategy '{mappingStrategy}' used for '{entityType}' is not supported for keyless entity types. + /// + public static string KeylessMappingStrategy(object? mappingStrategy, object? entityType) + => string.Format( + GetString("KeylessMappingStrategy", nameof(mappingStrategy), nameof(entityType)), + mappingStrategy, entityType); + /// /// Queries performing '{method}' operation must have a deterministic sort order. Rewrite the query to apply an 'OrderBy' operation on the sequence before calling '{method}'. /// @@ -844,7 +876,7 @@ public static string MissingParameterValue(object? parameter) parameter); /// - /// Cannot execute an ModificationCommandBatch which hasn't been completed. + /// Cannot add commands to a completed ModificationCommandBatch. /// public static string ModificationCommandBatchAlreadyComplete => GetString("ModificationCommandBatchAlreadyComplete"); @@ -853,7 +885,7 @@ public static string ModificationCommandBatchAlreadyComplete /// Cannot execute an ModificationCommandBatch which hasn't been completed. /// public static string ModificationCommandBatchNotComplete - => GetString("ModificationCommandBatchNotCompleted"); + => GetString("ModificationCommandBatchNotComplete"); /// /// Cannot save changes for an entity of type '{entityType}' in state '{entityState}'. This may indicate a bug in Entity Framework, please open an issue at https://go.microsoft.com/fwlink/?linkid=2142044. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the key values of the entity. @@ -932,7 +964,15 @@ public static string NonScalarFunctionParameterCannotPropagatesNullability(objec parameterName, functionName); /// - /// Both '{entityType}' and '{otherEntityType}' are mapped to the table '{table}'. All the entity types in a hierarchy that don't have a discriminator must be mapped to different tables. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. + /// The mapping strategy '{mappingStrategy}' specified on '{entityType}' is not supported for entity types with a discriminator. + /// + public static string NonTphMappingStrategy(object? mappingStrategy, object? entityType) + => string.Format( + GetString("NonTphMappingStrategy", nameof(mappingStrategy), nameof(entityType)), + mappingStrategy, entityType); + + /// + /// Both '{entityType}' and '{otherEntityType}' are mapped to the table '{table}'. All the entity types in a non-TPH hierarchy (one that doesn't have a discriminator) must be mapped to different tables. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. /// public static string NonTPHTableClash(object? entityType, object? otherEntityType, object? table) => string.Format( @@ -940,7 +980,7 @@ public static string NonTPHTableClash(object? entityType, object? otherEntityTyp entityType, otherEntityType, table); /// - /// Both '{entityType}' and '{otherEntityType}' are mapped to the view '{view}'. All the entity types in a hierarchy that don't have a discriminator must be mapped to different views. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. + /// Both '{entityType}' and '{otherEntityType}' are mapped to the view '{view}'. All the entity types in a non-TPH hierarchy (one that doesn't have a discriminator) must be mapped to different views. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. /// public static string NonTPHViewClash(object? entityType, object? otherEntityType, object? view) => string.Format( @@ -2056,6 +2096,28 @@ public static FallbackEventDefinition LogForeignKeyPropertiesMappedToUnrelatedTa return (FallbackEventDefinition)definition; } + /// + /// The foreign key {foreignKeyProperties} on the entity type '{entityType}' targeting '{principalEntityType}' cannot be represented in the database. '{principalEntityType}' is mapped using the table per concrete type meaning that the derived entities will not be present in {'principalTable'}. If this foreign key on '{entityType}' will never reference entities derived from '{principalEntityType}' then the foreign key constraint name can be specified explicitly to force it to be created. + /// + public static FallbackEventDefinition LogForeignKeyTPCPrincipal(IDiagnosticsLogger logger) + { + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogForeignKeyTPCPrincipal; + if (definition == null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((RelationalLoggingDefinitions)logger.Definitions).LogForeignKeyTPCPrincipal, + logger, + static logger => new FallbackEventDefinition( + logger.Options, + RelationalEventId.ForeignKeyTPCPrincipalWarning, + LogLevel.Warning, + "RelationalEventId.ForeignKeyTPCPrincipalWarning", + _resourceManager.GetString("LogForeignKeyTPCPrincipal")!)); + } + + return (FallbackEventDefinition)definition; + } + /// /// Generating down script for migration '{migration}'. /// @@ -2653,6 +2715,31 @@ public static EventDefinition LogRollingBackTransaction(IDiagnosticsLogger logge return (EventDefinition)definition; } + /// + /// The property '{property}' on entity type '{entityType}' is configured with a database-generated default, however the entity type is mapped to the database using table per concrete class strategy. Make sure that the generated values are unique across all the tables, duplicated values could result in errors or data corruption. + /// + public static EventDefinition LogTpcStoreGeneratedIdentity(IDiagnosticsLogger logger) + { + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogTpcStoreGeneratedIdentity; + if (definition == null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((RelationalLoggingDefinitions)logger.Definitions).LogTpcStoreGeneratedIdentity, + logger, + static logger => new EventDefinition( + logger.Options, + RelationalEventId.TpcStoreGeneratedIdentityWarning, + LogLevel.Warning, + "RelationalEventId.TpcStoreGeneratedIdentityWarning", + level => LoggerMessage.Define( + level, + RelationalEventId.TpcStoreGeneratedIdentityWarning, + _resourceManager.GetString("LogTpcStoreGeneratedIdentity")!))); + } + + return (EventDefinition)definition; + } + /// /// An error occurred using a transaction. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 9ba10958462..21a6a070bdd 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + The corresponding CLR type for entity type '{entityType}' cannot be instantiated, but the entity type was mapped to '{storeObject}' using the 'TPC' mapping strategy. Only instantiable types should be mapped. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. + Unable to deserialize a sequence from model metadata. See inner exception for details. Obsolete @@ -223,6 +226,9 @@ The number of key values ({valuesCount}) doesn't match the number of key columns ({columnsCount}) for the data deletion operation on '{table}'. Provide the same number of key values and key columns. + + The derived entity type '{entityType}' was configured with the '{strategy}' mapping strategy. Only the root entity type should be configured with a mapping strategy. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. + Using 'Distinct' operation on a projection containing a collection is not supported. @@ -400,12 +406,18 @@ The entity type '{entityType}' is mapped to a SQL query, but is derived from '{baseEntityType}'. Derived entity types cannot be mapped to a different SQL query than the base entity type. + + The mapping strategy '{mappingStrategy}' specified on '{entityType}' is not supported. + The specified 'MaxBatchSize' value '{value}' is not valid. It must be a positive number. The specified 'MinBatchSize' value '{value}' is not valid. It must be a positive number. + + The mapping strategy '{mappingStrategy}' used for '{entityType}' is not supported for keyless entity types. + Queries performing '{method}' operation must have a deterministic sort order. Rewrite the query to apply an 'OrderBy' operation on the sequence before calling '{method}'. @@ -529,6 +541,10 @@ The foreign key {foreignKeyProperties} on the entity type '{entityType}' targeting '{principalEntityType}' cannot be represented in the database. Either the properties {foreignKeyProperties} aren't mapped to table '{table}', or the principal properties {principalProperties} aren't mapped to table '{principalTable}'. All foreign key properties must map to the table to which the dependent type is mapped, and all principal properties must map to a single table to which the principal type is mapped. Error RelationalEventId.ForeignKeyPropertiesMappedToUnrelatedTables string string string string string string string + + The foreign key {foreignKeyProperties} on the entity type '{entityType}' targeting '{principalEntityType}' cannot be represented in the database. '{principalEntityType}' is mapped using the table per concrete type meaning that the derived entities will not be present in {'principalTable'}. If this foreign key on '{entityType}' will never reference entities derived from '{principalEntityType}' then the foreign key constraint name can be specified explicitly to force it to be created. + Warning RelationalEventId.ForeignKeyTPCPrincipalWarning string string string string string string string + Generating down script for migration '{migration}'. Debug RelationalEventId.MigrationGeneratingDownScript string @@ -625,6 +641,10 @@ Rolling back transaction. Debug RelationalEventId.TransactionRollingBack + + The property '{property}' on entity type '{entityType}' is configured with a database-generated default, however the entity type is mapped to the database using table per concrete class strategy. Make sure that the generated values are unique across all the tables, duplicated values could result in errors or data corruption. + Warning RelationalEventId.TpcStoreGeneratedIdentityWarning string string + An error occurred using a transaction. Error RelationalEventId.TransactionError @@ -708,11 +728,14 @@ Cannot set 'PropagatesNullability' on parameter '{parameterName}' of DbFunction '{functionName}' since function does not represent a scalar function. + + The mapping strategy '{mappingStrategy}' specified on '{entityType}' is not supported for entity types with a discriminator. + - Both '{entityType}' and '{otherEntityType}' are mapped to the table '{table}'. All the entity types in a hierarchy that don't have a discriminator must be mapped to different tables. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. + Both '{entityType}' and '{otherEntityType}' are mapped to the table '{table}'. All the entity types in a non-TPH hierarchy (one that doesn't have a discriminator) must be mapped to different tables. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. - Both '{entityType}' and '{otherEntityType}' are mapped to the view '{view}'. All the entity types in a hierarchy that don't have a discriminator must be mapped to different views. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. + Both '{entityType}' and '{otherEntityType}' are mapped to the view '{view}'. All the entity types in a non-TPH hierarchy (one that doesn't have a discriminator) must be mapped to different views. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. No relational database providers are configured. Configure a database provider using 'OnConfiguring' or by creating an ImmutableDbContextOptions with a configured database provider and passing it to the context. diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs index e77b22371af..adc6154513b 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs @@ -74,6 +74,10 @@ public void Test_new_annotations_handled_for_entity_types() RelationalAnnotationNames.IsFixedLength, RelationalAnnotationNames.Collation, RelationalAnnotationNames.IsStored, + RelationalAnnotationNames.MappingStrategy, // Will be handled in the next PR + RelationalAnnotationNames.TpcMappingStrategy, + RelationalAnnotationNames.TphMappingStrategy, + RelationalAnnotationNames.TptMappingStrategy, RelationalAnnotationNames.RelationalModel, RelationalAnnotationNames.ModelDependencies }; @@ -198,6 +202,10 @@ public void Test_new_annotations_handled_for_properties() RelationalAnnotationNames.Filter, RelationalAnnotationNames.DbFunctions, RelationalAnnotationNames.MaxIdentifierLength, + RelationalAnnotationNames.MappingStrategy, + RelationalAnnotationNames.TpcMappingStrategy, + RelationalAnnotationNames.TphMappingStrategy, + RelationalAnnotationNames.TptMappingStrategy, RelationalAnnotationNames.RelationalModel, RelationalAnnotationNames.ModelDependencies }; diff --git a/test/EFCore.Design.Tests/Migrations/Design/SnapshotModelProcessorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/SnapshotModelProcessorTest.cs index 00b234072b3..61ba71b3773 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/SnapshotModelProcessorTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/SnapshotModelProcessorTest.cs @@ -232,8 +232,9 @@ private void AddAnnotations(IMutableAnnotatable element) .Where( a => a != RelationalAnnotationNames.MaxIdentifierLength #pragma warning disable CS0618 // Type or member is obsolete - && a != RelationalAnnotationNames.SequencePrefix) + && a != RelationalAnnotationNames.SequencePrefix #pragma warning restore CS0618 // Type or member is obsolete + && a.IndexOf(':') > 0) .Select(a => "Unicorn" + a.Substring(RelationalAnnotationNames.Prefix.Length - 1))) { element[annotationName] = "Value"; @@ -261,8 +262,9 @@ private void AssertAnnotations(IMutableAnnotatable element) && a != RelationalAnnotationNames.UniqueConstraintMappings && a != RelationalAnnotationNames.RelationalOverrides #pragma warning disable CS0618 // Type or member is obsolete - && a != RelationalAnnotationNames.SequencePrefix)) + && a != RelationalAnnotationNames.SequencePrefix #pragma warning restore CS0618 // Type or member is obsolete + && a.IndexOf(':') > 0)) { Assert.Equal("Value", (string)element[annotationName]); } diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs index 5e365cfcd84..166946c8d5e 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs @@ -1019,6 +1019,7 @@ public static RuntimeForeignKey CreateForeignKey2(RuntimeEntityType declaringEnt public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) { + runtimeEntityType.AddAnnotation(""DiscriminatorMappingComplete"", false); runtimeEntityType.AddAnnotation(""Relational:FunctionName"", null); runtimeEntityType.AddAnnotation(""Relational:Schema"", null); runtimeEntityType.AddAnnotation(""Relational:SqlQuery"", null); @@ -1939,7 +1940,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) var dependentBase = dependentNavigation.TargetEntityType; - Assert.True(dependentBase.GetIsDiscriminatorMappingComplete()); + Assert.False(dependentBase.GetIsDiscriminatorMappingComplete()); var principalDiscriminator = dependentBase.FindDiscriminatorProperty(); Assert.IsType( principalDiscriminator.GetValueGeneratorFactory()(principalDiscriminator, dependentBase)); @@ -2130,7 +2131,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) eb.HasDiscriminator("EnumDiscriminator") .HasValue(Enum1.One) - .HasValue>(Enum1.Two); + .HasValue>(Enum1.Two) + .IsComplete(false); }); modelBuilder.Entity>( @@ -2147,6 +2149,436 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } } + [ConditionalFact] + public void TPC_model() + => Test( + new TpcContext(), + new CompiledModelCodeGenerationOptions { UseNullableReferenceTypes = true }, + code => + Assert.Collection( + code, + c => AssertFileContents( + "TpcContextModel.cs", + @"// +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Scaffolding.Internal; + +#pragma warning disable 219, 612, 618 +#nullable enable + +namespace TestNamespace +{ + [DbContext(typeof(CSharpRuntimeModelCodeGeneratorTest.TpcContext))] + public partial class TpcContextModel : RuntimeModel + { + static TpcContextModel() + { + var model = new TpcContextModel(); + model.Initialize(); + model.Customize(); + _instance = model; + } + + private static TpcContextModel _instance; + public static IModel Instance => _instance; + + partial void Initialize(); + + partial void Customize(); + } +} +", c), + c => AssertFileContents( + "TpcContextModelBuilder.cs", + @"// +using System; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; + +#pragma warning disable 219, 612, 618 +#nullable enable + +namespace TestNamespace +{ + public partial class TpcContextModel + { + partial void Initialize() + { + var dependentBasebyte = DependentBasebyteEntityType.Create(this); + var principalBase = PrincipalBaseEntityType.Create(this); + var principalDerivedDependentBasebyte = PrincipalDerivedDependentBasebyteEntityType.Create(this, principalBase); + + DependentBasebyteEntityType.CreateForeignKey1(dependentBasebyte, principalDerivedDependentBasebyte); + PrincipalBaseEntityType.CreateForeignKey1(principalBase, principalBase); + PrincipalBaseEntityType.CreateForeignKey2(principalBase, principalDerivedDependentBasebyte); + + DependentBasebyteEntityType.CreateAnnotations(dependentBasebyte); + PrincipalBaseEntityType.CreateAnnotations(principalBase); + PrincipalDerivedDependentBasebyteEntityType.CreateAnnotations(principalDerivedDependentBasebyte); + + AddAnnotation(""Relational:DefaultSchema"", ""TPC""); + AddAnnotation(""Relational:MaxIdentifierLength"", 128); + AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.IdentityColumn); + } + } +} +", c), + c => AssertFileContents( + "DependentBasebyteEntityType.cs", @"// +using System; +using System.Reflection; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Scaffolding.Internal; + +#pragma warning disable 219, 612, 618 +#nullable enable + +namespace TestNamespace +{ + internal partial class DependentBasebyteEntityType + { + public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? baseEntityType = null) + { + var runtimeEntityType = model.AddEntityType( + ""Microsoft.EntityFrameworkCore.Scaffolding.Internal.CSharpRuntimeModelCodeGeneratorTest+DependentBase"", + typeof(CSharpRuntimeModelCodeGeneratorTest.DependentBase), + baseEntityType); + + var id = runtimeEntityType.AddProperty( + ""Id"", + typeof(byte?), + propertyInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.DependentBase).GetProperty(""Id"", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.DependentBase).GetField(""k__BackingField"", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + afterSaveBehavior: PropertySaveBehavior.Throw); + id.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.None); + + var principalId = runtimeEntityType.AddProperty( + ""PrincipalId"", + typeof(long?), + nullable: true); + principalId.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.None); + + var key = runtimeEntityType.AddKey( + new[] { id }); + runtimeEntityType.SetPrimaryKey(key); + + var index = runtimeEntityType.AddIndex( + new[] { principalId }, + unique: true); + + return runtimeEntityType; + } + + public static RuntimeForeignKey CreateForeignKey1(RuntimeEntityType declaringEntityType, RuntimeEntityType principalEntityType) + { + var runtimeForeignKey = declaringEntityType.AddForeignKey(new[] { declaringEntityType.FindProperty(""PrincipalId"")! }, + principalEntityType.FindKey(new[] { principalEntityType.FindProperty(""Id"")! })!, + principalEntityType, + deleteBehavior: DeleteBehavior.ClientCascade, + unique: true, + requiredDependent: true); + + var principal = declaringEntityType.AddNavigation(""Principal"", + runtimeForeignKey, + onDependent: true, + typeof(CSharpRuntimeModelCodeGeneratorTest.PrincipalDerived>), + propertyInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.DependentBase).GetProperty(""Principal"", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.DependentBase).GetField(""k__BackingField"", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + + var dependent = principalEntityType.AddNavigation(""Dependent"", + runtimeForeignKey, + onDependent: false, + typeof(CSharpRuntimeModelCodeGeneratorTest.DependentBase), + propertyInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.PrincipalDerived>).GetProperty(""Dependent"", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.PrincipalDerived>).GetField(""k__BackingField"", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + + return runtimeForeignKey; + } + + public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) + { + runtimeEntityType.AddAnnotation(""Relational:FunctionName"", null); + runtimeEntityType.AddAnnotation(""Relational:Schema"", ""TPC""); + runtimeEntityType.AddAnnotation(""Relational:SqlQuery"", null); + runtimeEntityType.AddAnnotation(""Relational:TableName"", ""DependentBase""); + runtimeEntityType.AddAnnotation(""Relational:ViewName"", null); + runtimeEntityType.AddAnnotation(""Relational:ViewSchema"", null); + + Customize(runtimeEntityType); + } + + static partial void Customize(RuntimeEntityType runtimeEntityType); + } +} +", c), + c => AssertFileContents( + "PrincipalBaseEntityType.cs", @"// +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Scaffolding.Internal; + +#pragma warning disable 219, 612, 618 +#nullable enable + +namespace TestNamespace +{ + internal partial class PrincipalBaseEntityType + { + public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? baseEntityType = null) + { + var runtimeEntityType = model.AddEntityType( + ""Microsoft.EntityFrameworkCore.Scaffolding.Internal.CSharpRuntimeModelCodeGeneratorTest+PrincipalBase"", + typeof(CSharpRuntimeModelCodeGeneratorTest.PrincipalBase), + baseEntityType); + + var id = runtimeEntityType.AddProperty( + ""Id"", + typeof(long?), + propertyInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.PrincipalBase).GetProperty(""Id"", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.PrincipalBase).GetField(""k__BackingField"", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + valueGenerated: ValueGenerated.OnAdd, + afterSaveBehavior: PropertySaveBehavior.Throw); + id.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.IdentityColumn); + + var principalBaseId = runtimeEntityType.AddProperty( + ""PrincipalBaseId"", + typeof(long?), + nullable: true); + principalBaseId.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.None); + + var principalDerivedDependentBasebyteId = runtimeEntityType.AddProperty( + ""PrincipalDerived>Id"", + typeof(long?), + nullable: true); + principalDerivedDependentBasebyteId.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.None); + + var key = runtimeEntityType.AddKey( + new[] { id }); + runtimeEntityType.SetPrimaryKey(key); + + var index = runtimeEntityType.AddIndex( + new[] { principalBaseId }); + + var index0 = runtimeEntityType.AddIndex( + new[] { principalDerivedDependentBasebyteId }); + + return runtimeEntityType; + } + + public static RuntimeForeignKey CreateForeignKey1(RuntimeEntityType declaringEntityType, RuntimeEntityType principalEntityType) + { + var runtimeForeignKey = declaringEntityType.AddForeignKey(new[] { declaringEntityType.FindProperty(""PrincipalBaseId"")! }, + principalEntityType.FindKey(new[] { principalEntityType.FindProperty(""Id"")! })!, + principalEntityType); + + var deriveds = principalEntityType.AddNavigation(""Deriveds"", + runtimeForeignKey, + onDependent: false, + typeof(ICollection), + propertyInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.PrincipalBase).GetProperty(""Deriveds"", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.PrincipalBase).GetField(""k__BackingField"", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + + return runtimeForeignKey; + } + + public static RuntimeForeignKey CreateForeignKey2(RuntimeEntityType declaringEntityType, RuntimeEntityType principalEntityType) + { + var runtimeForeignKey = declaringEntityType.AddForeignKey(new[] { declaringEntityType.FindProperty(""PrincipalDerived>Id"")! }, + principalEntityType.FindKey(new[] { principalEntityType.FindProperty(""Id"")! })!, + principalEntityType); + + var principals = principalEntityType.AddNavigation(""Principals"", + runtimeForeignKey, + onDependent: false, + typeof(ICollection), + propertyInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.PrincipalDerived>).GetProperty(""Principals"", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.PrincipalDerived>).GetField(""k__BackingField"", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + + return runtimeForeignKey; + } + + public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) + { + runtimeEntityType.AddAnnotation(""Relational:FunctionName"", null); + runtimeEntityType.AddAnnotation(""Relational:MappingStrategy"", ""TPC""); + runtimeEntityType.AddAnnotation(""Relational:Schema"", ""TPC""); + runtimeEntityType.AddAnnotation(""Relational:SqlQuery"", null); + runtimeEntityType.AddAnnotation(""Relational:TableName"", ""PrincipalBase""); + runtimeEntityType.AddAnnotation(""Relational:ViewDefinitionSql"", null); + runtimeEntityType.AddAnnotation(""Relational:ViewName"", ""PrincipalBaseView""); + runtimeEntityType.AddAnnotation(""Relational:ViewSchema"", ""TPC""); + + Customize(runtimeEntityType); + } + + static partial void Customize(RuntimeEntityType runtimeEntityType); + } +} +", c), + c => AssertFileContents( + "PrincipalDerivedDependentBasebyteEntityType.cs", @"// +using System; +using System.Reflection; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Scaffolding.Internal; + +#pragma warning disable 219, 612, 618 +#nullable enable + +namespace TestNamespace +{ + internal partial class PrincipalDerivedDependentBasebyteEntityType + { + public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? baseEntityType = null) + { + var runtimeEntityType = model.AddEntityType( + ""Microsoft.EntityFrameworkCore.Scaffolding.Internal.CSharpRuntimeModelCodeGeneratorTest+PrincipalDerived>"", + typeof(CSharpRuntimeModelCodeGeneratorTest.PrincipalDerived>), + baseEntityType); + + return runtimeEntityType; + } + + public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) + { + runtimeEntityType.AddAnnotation(""Relational:FunctionName"", null); + runtimeEntityType.AddAnnotation(""Relational:Schema"", ""TPC""); + runtimeEntityType.AddAnnotation(""Relational:SqlQuery"", null); + runtimeEntityType.AddAnnotation(""Relational:TableName"", ""PrincipalDerived""); + runtimeEntityType.AddAnnotation(""Relational:ViewDefinitionSql"", null); + runtimeEntityType.AddAnnotation(""Relational:ViewName"", ""PrincipalDerivedView""); + runtimeEntityType.AddAnnotation(""Relational:ViewSchema"", ""TPC""); + + Customize(runtimeEntityType); + } + + static partial void Customize(RuntimeEntityType runtimeEntityType); + } +} +", c)), + model => + { + Assert.Equal("TPC", model.GetDefaultSchema()); + + var principalBase = model.FindEntityType(typeof(PrincipalBase)); + Assert.Equal("PrincipalBase", principalBase.GetTableName()); + Assert.Equal("TPC", principalBase.GetSchema()); + Assert.Equal("PrincipalBaseView", principalBase.GetViewName()); + Assert.Equal("TPC", principalBase.GetViewSchema()); + + Assert.Null(principalBase.GetDiscriminatorValue()); + Assert.Null(principalBase.FindDiscriminatorProperty()); + Assert.Equal("TPC", principalBase.GetMappingStrategy()); + + var selfRefNavigation = principalBase.GetDeclaredNavigations().Last(); + Assert.Equal("Deriveds", selfRefNavigation.Name); + Assert.True(selfRefNavigation.IsCollection); + Assert.False(selfRefNavigation.IsOnDependent); + Assert.Equal(principalBase, selfRefNavigation.TargetEntityType); + Assert.Null(selfRefNavigation.Inverse); + + var principalDerived = model.FindEntityType(typeof(PrincipalDerived>)); + Assert.Equal(principalBase, principalDerived.BaseType); + + Assert.Equal("PrincipalDerived", principalDerived.GetTableName()); + Assert.Equal("TPC", principalDerived.GetSchema()); + Assert.Equal("PrincipalDerivedView", principalDerived.GetViewName()); + Assert.Equal("TPC", principalBase.GetViewSchema()); + + Assert.Null(principalDerived.GetDiscriminatorValue()); + Assert.Null(principalDerived.FindDiscriminatorProperty()); + Assert.Equal("TPC", principalDerived.GetMappingStrategy()); + + Assert.Equal(2, principalDerived.GetDeclaredNavigations().Count()); + var derivedNavigation = principalDerived.GetDeclaredNavigations().Last(); + Assert.Equal("Principals", derivedNavigation.Name); + Assert.True(derivedNavigation.IsCollection); + Assert.False(derivedNavigation.IsOnDependent); + Assert.Equal(principalBase, derivedNavigation.TargetEntityType); + Assert.Null(derivedNavigation.Inverse); + + var dependentNavigation = principalDerived.GetDeclaredNavigations().First(); + Assert.Equal("Dependent", dependentNavigation.Name); + Assert.Equal("Dependent", dependentNavigation.PropertyInfo.Name); + Assert.Equal("k__BackingField", dependentNavigation.FieldInfo.Name); + Assert.False(dependentNavigation.IsCollection); + Assert.False(dependentNavigation.IsEagerLoaded); + Assert.False(dependentNavigation.IsOnDependent); + Assert.Equal(principalDerived, dependentNavigation.DeclaringEntityType); + Assert.Equal("Principal", dependentNavigation.Inverse.Name); + + var dependentForeignKey = dependentNavigation.ForeignKey; + Assert.False(dependentForeignKey.IsOwnership); + Assert.False(dependentForeignKey.IsRequired); + Assert.True(dependentForeignKey.IsRequiredDependent); + Assert.True(dependentForeignKey.IsUnique); + Assert.Same(principalDerived, dependentForeignKey.PrincipalEntityType); + Assert.Same(dependentNavigation.Inverse, dependentForeignKey.DependentToPrincipal); + Assert.Same(dependentNavigation, dependentForeignKey.PrincipalToDependent); + Assert.Equal(DeleteBehavior.ClientCascade, dependentForeignKey.DeleteBehavior); + Assert.Equal(new[] { "PrincipalId" }, dependentForeignKey.Properties.Select(p => p.Name)); + + var dependentBase = dependentNavigation.TargetEntityType; + + Assert.True(dependentBase.GetIsDiscriminatorMappingComplete()); + Assert.Null(dependentBase.FindDiscriminatorProperty()); + + Assert.Same(dependentForeignKey, dependentBase.GetForeignKeys().Single()); + + Assert.Equal( + new[] + { + dependentBase, + principalBase, + principalDerived + }, + model.GetEntityTypes()); + }, + typeof(SqlServerNetTopologySuiteDesignTimeServices)); + + public class TpcContext : SqlServerContextBase + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.HasDefaultSchema("TPC"); + + modelBuilder.Entity( + eb => + { + eb.Ignore(e => e.Owned); + + eb.UseTpcMappingStrategy(); + eb.ToTable("PrincipalBase"); + eb.ToView("PrincipalBaseView"); + }); + + modelBuilder.Entity>>( + eb => + { + eb.HasOne(e => e.Dependent).WithOne(e => e.Principal) + .HasForeignKey>() + .OnDelete(DeleteBehavior.ClientCascade); + + eb.Navigation(e => e.Dependent).IsRequired(); + + eb.ToTable("PrincipalDerived"); + eb.ToView("PrincipalDerivedView"); + }); + + modelBuilder.Entity>( + eb => + { + eb.Property("Id"); + }); + } + } + public class CustomValueComparer : ValueComparer { public CustomValueComparer() @@ -3623,11 +4055,6 @@ protected void Test( var assembly = build.BuildInMemory(); - if (assertScaffold != null) - { - assertScaffold(scaffoldedFiles); - } - if (assertModel != null) { var modelType = assembly.GetType(options.ModelNamespace + "." + options.ContextType.Name + "Model"); @@ -3637,6 +4064,11 @@ protected void Test( var modelRuntimeInitializer = context.GetService(); assertModel(modelRuntimeInitializer.Initialize(compiledModel, designTime: false)); } + + if (assertScaffold != null) + { + assertScaffold(scaffoldedFiles); + } } protected static void AssertFileContents( diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index b50fdd57f5c..9ce26453f6b 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs @@ -73,9 +73,7 @@ public override void Detects_missing_discriminator_property() var entityC = model.AddEntityType(typeof(C)); entityC.BaseType = entityA; - VerifyError( - RelationalStrings.NonTPHTableClash( - entityC.DisplayName(), entityA.DisplayName(), entityA.DisplayName()), modelBuilder); + Validate(modelBuilder); } [ConditionalFact] @@ -1572,16 +1570,14 @@ public virtual void Passes_for_TPT() } [ConditionalFact] - public virtual void Detects_unconfigured_entity_type_in_TPT() + public virtual void Passes_for_unconfigured_entity_type_in_TPT() { var modelBuilder = CreateConventionalModelBuilder(); modelBuilder.Entity(); modelBuilder.Entity().ToTable("Cat"); modelBuilder.Entity(); - VerifyError( - RelationalStrings.NonTPHTableClash(nameof(Dog), nameof(Animal), nameof(Animal)), - modelBuilder); + Validate(modelBuilder); } [ConditionalFact] @@ -1633,6 +1629,18 @@ public virtual void Detects_view_TPT_with_discriminator() modelBuilder); } + [ConditionalFact] + public virtual void Detects_TPT_with_keyless_entity_type() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().HasNoKey(); + modelBuilder.Entity().ToTable("Cat"); + + VerifyError( + RelationalStrings.KeylessMappingStrategy("TPT", nameof(Animal)), + modelBuilder); + } + [ConditionalFact] public virtual void Passes_on_valid_table_sharing_with_TPT() { @@ -1725,6 +1733,261 @@ public virtual void Detects_unmapped_foreign_keys_in_TPT() LogLevel.Error); } + [ConditionalFact] + public virtual void Passes_for_ToTable_for_abstract_class() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().ToTable("Abstract"); + modelBuilder.Entity(); + modelBuilder.Entity>(); + + Validate(modelBuilder); + } + + [ConditionalFact] + public virtual void Passes_for_abstract_class_TPC() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity(); + modelBuilder.Entity().UseTpcMappingStrategy(); + modelBuilder.Entity>().ToTable("G"); + + Validate(modelBuilder); + } + + [ConditionalFact] + public virtual void Passes_for_view_TPC() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().ToTable((string)null).UseTpcMappingStrategy(); + modelBuilder.Entity().ToView("Cat"); + + Validate(modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_invalid_MappingStrategy() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().HasAnnotation(RelationalAnnotationNames.MappingStrategy, "TTT"); + modelBuilder.Entity(); + + VerifyError( + RelationalStrings.InvalidMappingStrategy("TTT", nameof(Animal)), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_MappingStrategy_on_derived_types() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().HasBaseType((string)null); + modelBuilder.Entity(); + modelBuilder.Entity().ToTable("Cat").ToView("Cat").UseTpcMappingStrategy().HasBaseType(typeof(Animal)); + + VerifyError( + RelationalStrings.DerivedStrategy(nameof(Cat), "TPC"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_ToTable_for_abstract_class_TPC() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().ToTable("Abstract", "dbo").UseTpcMappingStrategy(); + modelBuilder.Entity(); + modelBuilder.Entity>(); + + VerifyError( + RelationalStrings.AbstractTPC(nameof(Abstract), "dbo.Abstract"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_ToView_for_abstract_class_TPC() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().ToView("Abstract").UseTpcMappingStrategy(); + modelBuilder.Entity(); + modelBuilder.Entity>(); + + VerifyError( + RelationalStrings.AbstractTPC(nameof(Abstract), "Abstract"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_ToFunction_for_abstract_class_TPC() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().ToFunction("Abstract").UseTpcMappingStrategy(); + modelBuilder.Entity(); + modelBuilder.Entity>(); + + VerifyError( + RelationalStrings.AbstractTPC(nameof(Abstract), "Abstract"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_clashing_entity_types_in_views_TPC() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().UseTpcMappingStrategy(); + modelBuilder.Entity().ToTable("Cat").ToView("Cat"); + modelBuilder.Entity().ToTable("Dog").ToView("Cat"); + + VerifyError( + RelationalStrings.NonTPHViewClash(nameof(Dog), nameof(Cat), "Cat"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_table_and_view_TPC_mismatch() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().UseTpcMappingStrategy().ToTable("Animal").ToView("Animal"); + modelBuilder.Entity().ToTable("Animal").ToView("Cat"); + + VerifyError( + RelationalStrings.NonTPHTableClash(nameof(Cat), nameof(Animal), "Animal"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Passes_on_TPC_with_keyless_entity_type() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().UseTpcMappingStrategy().HasNoKey(); + modelBuilder.Entity().ToTable("Cat"); + + Validate(modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_view_TPC_with_discriminator() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().ToView("Animal").UseTpcMappingStrategy().HasDiscriminator("Discriminator"); + modelBuilder.Entity().ToView("Cat"); + + VerifyError( + RelationalStrings.NonTphMappingStrategy("TPC", nameof(Animal)), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_store_generated_PK_in_TPC() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().UseTpcMappingStrategy(); + modelBuilder.Entity(); + + var definition = + RelationalResources.LogTpcStoreGeneratedIdentity(new TestLogger()); + VerifyWarning( + definition.GenerateMessage(nameof(Animal.Id), nameof(Animal)), + modelBuilder, + LogLevel.Warning); + } + + [ConditionalFact] + public virtual void Passes_on_valid_view_sharing_with_TPC() + { + var modelBuilder = CreateConventionalModelBuilder(); + + modelBuilder.Entity() + .UseTpcMappingStrategy() + .ToView("Animal") + .Ignore(a => a.FavoritePerson); + + modelBuilder.Entity( + x => + { + x.ToView("Cat"); + x.HasOne(c => c.FavoritePerson).WithOne().HasForeignKey(c => c.Id); + }); + + modelBuilder.Entity().ToView("Cat"); + + Validate(modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_linking_relationship_on_derived_type_in_TPC() + { + var modelBuilder = CreateConventionalModelBuilder(); + + modelBuilder.Entity() + .UseTpcMappingStrategy() + .Ignore(a => a.FavoritePerson); + + modelBuilder.Entity( + x => + { + x.ToTable("Cat"); + x.HasOne(c => c.FavoritePerson).WithOne().HasForeignKey(c => c.Id); + }); + + modelBuilder.Entity().ToTable("Cat"); + + VerifyError( + RelationalStrings.IncompatibleTableDerivedRelationship( + "Cat", "Cat", "Person"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_linking_relationship_on_derived_type_in_TPC_views() + { + var modelBuilder = CreateConventionalModelBuilder(); + + modelBuilder.Entity() + .UseTpcMappingStrategy() + .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); + } + + [ConditionalFact] + public virtual void Detects_unmapped_foreign_keys_in_TPC() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().UseTpcMappingStrategy() + .Property(a => a.Id).ValueGeneratedNever(); + modelBuilder.Entity().ToTable("Cat"); + modelBuilder.Entity() + .HasOne().WithOne(a => a.FavoritePerson) + .HasForeignKey(p => p.FavoriteBreed) + .HasPrincipalKey(a => a.Name); + + var definition = + RelationalResources.LogForeignKeyTPCPrincipal(new TestLogger()); + VerifyWarning( + definition.GenerateMessage( + l => l.Log( + definition.Level, + definition.EventId, + definition.MessageFormat, + "{'FavoriteBreed'}", nameof(Person), nameof(Animal), nameof(Animal), nameof(Animal), nameof(Person), + nameof(Animal))), + modelBuilder, + LogLevel.Warning); + } + [ConditionalFact] public virtual void Passes_for_valid_table_overrides() { @@ -2008,13 +2271,27 @@ public void Passes_for_named_index_with_all_properties_not_mapped_to_any_table() } [ConditionalFact] - public void Detects_mix_of_index_property_mapped_and_not_mapped_to_any_table_unmapped_first() + public void Passes_for_mix_of_index_properties_declared_and_inherited_TPT() { var modelBuilder = CreateConventionalModelBuilder(); modelBuilder.Entity().ToTable((string)null); - modelBuilder.Entity().ToTable("Cats"); - modelBuilder.Entity().HasIndex(nameof(Animal.Name), nameof(Cat.Identity)); + modelBuilder.Entity().ToTable("Cats") + .HasIndex( + new[] { nameof(Cat.Identity), nameof(Animal.Name) }, + "IX_MixOfMappedAndUnmappedProperties"); + + Validate(modelBuilder); + } + + [ConditionalFact] + public void Detects_mix_of_index_property_mapped_and_not_mapped_to_any_table_mapped_first() + { + var modelBuilder = CreateConventionalModelBuilder(); + + modelBuilder.Entity(); + modelBuilder.Entity().ToTable((string)null) + .HasIndex(nameof(Animal.Name), nameof(Cat.Identity)); var definition = RelationalResources .LogUnnamedIndexPropertiesBothMappedAndNotMappedToTable( @@ -2023,19 +2300,18 @@ public void Detects_mix_of_index_property_mapped_and_not_mapped_to_any_table_unm definition.GenerateMessage( nameof(Cat), "{'Name', 'Identity'}", - "Name"), + "Identity"), modelBuilder, LogLevel.Error); } [ConditionalFact] - public void Detects_mix_of_index_property_mapped_and_not_mapped_to_any_table_mapped_first() + public void Detects_mix_of_index_property_mapped_and_not_mapped_to_any_table_unmapped_first() { var modelBuilder = CreateConventionalModelBuilder(); - modelBuilder.Entity().ToTable((string)null); - modelBuilder.Entity().ToTable("Cats"); - modelBuilder.Entity() + modelBuilder.Entity(); + modelBuilder.Entity().ToTable((string)null) .HasIndex( new[] { nameof(Cat.Identity), nameof(Animal.Name) }, "IX_MixOfMappedAndUnmappedProperties"); @@ -2048,7 +2324,7 @@ public void Detects_mix_of_index_property_mapped_and_not_mapped_to_any_table_map "IX_MixOfMappedAndUnmappedProperties", nameof(Cat), "{'Identity', 'Name'}", - "Name"), + "Identity"), modelBuilder, LogLevel.Error); } @@ -2059,8 +2335,8 @@ public void Passes_for_index_properties_mapped_to_same_table_in_TPT_hierarchy() var modelBuilder = CreateConventionalModelBuilder(); modelBuilder.Entity().ToTable("Animals"); - modelBuilder.Entity().ToTable("Cats"); - modelBuilder.Entity().HasIndex(nameof(Animal.Id), nameof(Cat.Identity)); + modelBuilder.Entity().ToTable("Cats") + .HasIndex(nameof(Animal.Id), nameof(Cat.Identity)); Validate(modelBuilder); diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs index 3bc63e87639..3d95323f8cc 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs @@ -23,54 +23,74 @@ public void GetRelationalModel_throws_if_convention_has_not_run() [ConditionalTheory] [InlineData(true, Mapping.TPH)] [InlineData(true, Mapping.TPT)] + [InlineData(true, Mapping.TPC)] [InlineData(false, Mapping.TPH)] [InlineData(false, Mapping.TPT)] + [InlineData(false, Mapping.TPC)] public void Can_use_relational_model_with_tables(bool useExplicitMapping, Mapping mapping) { var model = CreateTestModel(mapToTables: useExplicitMapping, mapping: mapping); - Assert.Equal(9, model.Model.GetEntityTypes().Count()); - Assert.Equal(mapping == Mapping.TPH || !useExplicitMapping ? 3 : 5, model.Tables.Count()); + Assert.Equal(11, model.Model.GetEntityTypes().Count()); + Assert.Equal(mapping == Mapping.TPC + ? 5 + : mapping == Mapping.TPH + ? 3 + : 6, model.Tables.Count()); Assert.Empty(model.Views); Assert.True(model.Model.GetEntityTypes().All(et => !et.GetViewMappings().Any())); - AssertDefaultMappings(model); - AssertTables(model, useExplicitMapping ? mapping : Mapping.TPH); + AssertDefaultMappings(model, mapping); + AssertTables(model, mapping); } [ConditionalTheory] [InlineData(Mapping.TPH)] [InlineData(Mapping.TPT)] + [InlineData(Mapping.TPC)] public void Can_use_relational_model_with_views(Mapping mapping) { var model = CreateTestModel(mapToTables: false, mapToViews: true, mapping); - Assert.Equal(9, model.Model.GetEntityTypes().Count()); - Assert.Equal(mapping == Mapping.TPH ? 3 : 5, model.Views.Count()); + Assert.Equal(11, model.Model.GetEntityTypes().Count()); + Assert.Equal(mapping == Mapping.TPC + ? 5 + : mapping == Mapping.TPH + ? 3 + : 6, model.Views.Count()); Assert.Empty(model.Tables); Assert.True(model.Model.GetEntityTypes().All(et => !et.GetTableMappings().Any())); - AssertDefaultMappings(model); + AssertDefaultMappings(model, mapping); AssertViews(model, mapping); } [ConditionalTheory] [InlineData(Mapping.TPH)] [InlineData(Mapping.TPT)] + [InlineData(Mapping.TPC)] public void Can_use_relational_model_with_views_and_tables(Mapping mapping) { var model = CreateTestModel(mapToTables: true, mapToViews: true, mapping); - Assert.Equal(9, model.Model.GetEntityTypes().Count()); - Assert.Equal(mapping == Mapping.TPH ? 3 : 5, model.Tables.Count()); - Assert.Equal(mapping == Mapping.TPH ? 3 : 5, model.Views.Count()); - - AssertDefaultMappings(model); + Assert.Equal(11, model.Model.GetEntityTypes().Count()); + Assert.Equal(mapping == Mapping.TPC + ? 5 + : mapping == Mapping.TPH + ? 3 + : 6, model.Tables.Count()); + Assert.Equal(mapping == Mapping.TPC + ? 5 + : mapping == Mapping.TPH + ? 3 + : 6, model.Views.Count()); + + AssertDefaultMappings(model, mapping); AssertTables(model, mapping); AssertViews(model, mapping); } - private static void AssertDefaultMappings(IRelationalModel model) + private static void AssertDefaultMappings(IRelationalModel model, Mapping mapping) { var orderType = model.Model.FindEntityType(typeof(Order)); var orderMapping = orderType.GetDefaultMappings().Single(); @@ -94,14 +114,20 @@ private static void AssertDefaultMappings(IRelationalModel model) Assert.Equal("default_datetime_mapping", orderDateMapping.TypeMapping.StoreType); Assert.Same(orderMapping, orderDateMapping.TableMapping); + var abstractBaseType = model.Model.FindEntityType(typeof(AbstractBase)); + var abstractCustomerType = model.Model.FindEntityType(typeof(AbstractCustomer)); + var customerType = model.Model.FindEntityType(typeof(Customer)); + var specialCustomerType = model.Model.FindEntityType(typeof(SpecialCustomer)); + var extraSpecialCustomerType = model.Model.FindEntityType(typeof(ExtraSpecialCustomer)); var orderDetailsOwnership = orderType.FindNavigation(nameof(Order.Details)).ForeignKey; var orderDetailsType = orderDetailsOwnership.DeclaringEntityType; var orderDetailsTable = orderDetailsType.GetDefaultMappings().Single().Table; Assert.NotEqual(ordersTable, orderDetailsTable); Assert.Empty(ordersTable.GetReferencingRowInternalForeignKeys(orderType)); - - var orderDetailsDate = orderDetailsType.FindProperty(nameof(OrderDetails.OrderDate)); - Assert.Equal(new[] { orderDetailsDate }, orderDetailsTable.FindColumn("OrderDate").PropertyMappings.Select(m => m.Property)); + Assert.Equal( + RelationalStrings.TableNotMappedEntityType(nameof(SpecialCustomer), ordersTable.Name), + Assert.Throws( + () => ordersTable.IsOptional(specialCustomerType)).Message); var orderDateColumn = orderDateMapping.Column; Assert.Same(orderDateColumn, ordersTable.FindColumn("OrderDate")); @@ -112,35 +138,92 @@ private static void AssertDefaultMappings(IRelationalModel model) Assert.False(orderDateColumn.IsNullable); Assert.Same(ordersTable, orderDateColumn.Table); - var customerType = model.Model.FindEntityType(typeof(Customer)); - var customerTable = customerType.GetDefaultMappings().Single().Table; - Assert.Equal("Microsoft.EntityFrameworkCore.Metadata.RelationalModelTest+Customer", customerTable.Name); - Assert.Null(customerTable.Schema); + var orderDetailsDate = orderDetailsType.FindProperty(nameof(OrderDetails.OrderDate)); + Assert.Equal(new[] { orderDetailsDate }, orderDetailsTable.FindColumn("OrderDate").PropertyMappings.Select(m => m.Property)); - var specialCustomerType = model.Model.FindEntityType(typeof(SpecialCustomer)); - var customerPk = specialCustomerType.FindPrimaryKey(); + var customerTable = customerType.GetDefaultMappings().Last().Table; + Assert.False(customerTable.IsOptional(customerType)); + if (mapping == Mapping.TPC) + { + Assert.Equal( + RelationalStrings.TableNotMappedEntityType(nameof(SpecialCustomer), customerTable.Name), + Assert.Throws( + () => customerTable.IsOptional(specialCustomerType)).Message); + } + else + { + Assert.False(customerTable.IsOptional(specialCustomerType)); + Assert.False(customerTable.IsOptional(extraSpecialCustomerType)); + } + + if (mapping == Mapping.TPT) + { + Assert.Equal("Microsoft.EntityFrameworkCore.Metadata.RelationalModelTest+Customer", customerTable.Name); + Assert.Null(customerTable.Schema); + Assert.Equal(4, specialCustomerType.GetDefaultMappings().Count()); + Assert.True(specialCustomerType.GetDefaultMappings().First().IsSplitEntityTypePrincipal); + Assert.False(specialCustomerType.GetDefaultMappings().First().IncludesDerivedTypes); + Assert.True(specialCustomerType.GetDefaultMappings().Last().IsSplitEntityTypePrincipal); + Assert.True(specialCustomerType.GetDefaultMappings().Last().IncludesDerivedTypes); + + var specialCustomerTable = specialCustomerType.GetDefaultMappings().Last().Table; + Assert.Null(specialCustomerTable.Schema); + Assert.Equal(4, specialCustomerTable.Columns.Count()); + + Assert.True(specialCustomerTable.EntityTypeMappings.Single(m => m.EntityType == specialCustomerType).IsSharedTablePrincipal); + + var specialityColumn = specialCustomerTable.Columns.Single(c => c.Name == nameof(SpecialCustomer.Speciality)); + Assert.False(specialityColumn.IsNullable); - var specialCustomerDefaultMapping = specialCustomerType.GetDefaultMappings().Single(); - Assert.True(specialCustomerDefaultMapping.IsSplitEntityTypePrincipal); - Assert.True(specialCustomerDefaultMapping.IncludesDerivedTypes); + Assert.Null(customerType.FindDiscriminatorProperty()); + Assert.Null(customerType.GetDiscriminatorValue()); + Assert.Null(specialCustomerType.FindDiscriminatorProperty()); + Assert.Null(specialCustomerType.GetDiscriminatorValue()); + } + else + { + var specialCustomerTableMapping = specialCustomerType.GetDefaultMappings().Single(); + Assert.True(specialCustomerTableMapping.IsSplitEntityTypePrincipal); + var specialCustomerTable = specialCustomerTableMapping.Table; + var specialityColumn = specialCustomerTable.Columns.Single(c => c.Name == nameof(SpecialCustomer.Speciality)); + if (mapping == Mapping.TPH) + { + var baseTable = abstractBaseType.GetDefaultMappings().Single().Table; + Assert.Equal("Microsoft.EntityFrameworkCore.Metadata.RelationalModelTest+AbstractBase", baseTable.Name); + Assert.Equal(baseTable.Name, customerTable.Name); + Assert.Equal(baseTable.Schema, customerTable.Schema); + Assert.True(specialCustomerTableMapping.IncludesDerivedTypes); + Assert.Same(customerTable, specialCustomerTable); - var specialCustomerTable = specialCustomerDefaultMapping.Table; - Assert.Equal(customerTable, specialCustomerTable); + Assert.Equal(5, specialCustomerTable.EntityTypeMappings.Count()); + Assert.True(specialCustomerTable.EntityTypeMappings.All(t => t.IsSharedTablePrincipal)); - Assert.Equal(3, specialCustomerTable.EntityTypeMappings.Count()); - Assert.True(specialCustomerTable.EntityTypeMappings.First().IsSharedTablePrincipal); + Assert.Equal(10, specialCustomerTable.Columns.Count()); + + Assert.True(specialityColumn.IsNullable); + } + else + { + Assert.False(specialCustomerTableMapping.IncludesDerivedTypes); + Assert.NotSame(customerTable, specialCustomerTable); - Assert.Equal(specialCustomerType.FindDiscriminatorProperty() == null ? 8 : 9, specialCustomerTable.Columns.Count()); + Assert.True(customerTable.EntityTypeMappings.Single().IsSharedTablePrincipal); + Assert.Equal(5, customerTable.Columns.Count()); - var specialityColumn = specialCustomerTable.Columns.Single(c => c.Name == nameof(SpecialCustomer.Speciality)); - Assert.Equal(specialCustomerType.FindDiscriminatorProperty() != null, specialityColumn.IsNullable); + Assert.True(specialCustomerTable.EntityTypeMappings.Single().IsSharedTablePrincipal); + + Assert.Equal(9, specialCustomerTable.Columns.Count()); + + Assert.False(specialityColumn.IsNullable); + } + } } private static void AssertViews(IRelationalModel model, Mapping mapping) { var orderType = model.Model.FindEntityType(typeof(Order)); var orderMapping = orderType.GetViewMappings().Single(); - Assert.Same(orderType.GetViewMappings(), orderType.GetViewOrTableMappings()); + Assert.Equal(orderType.GetViewMappings(), orderType.GetViewOrTableMappings()); Assert.True(orderMapping.IncludesDerivedTypes); Assert.Equal( new[] { nameof(Order.Id), nameof(Order.AlternateId), nameof(Order.CustomerId), nameof(Order.OrderDate) }, @@ -180,49 +263,71 @@ private static void AssertViews(IRelationalModel model, Mapping mapping) Assert.Equal("default_datetime_mapping", orderDateMapping.TypeMapping.StoreType); Assert.Same(orderMapping, orderDateMapping.ViewMapping); + var abstractBaseType = model.Model.FindEntityType(typeof(AbstractBase)); + var abstractCustomerType = model.Model.FindEntityType(typeof(AbstractCustomer)); + var customerType = model.Model.FindEntityType(typeof(Customer)); + var specialCustomerType = model.Model.FindEntityType(typeof(SpecialCustomer)); + var extraSpecialCustomerType = model.Model.FindEntityType(typeof(ExtraSpecialCustomer)); var orderDetailsOwnership = orderType.FindNavigation(nameof(Order.Details)).ForeignKey; var orderDetailsType = orderDetailsOwnership.DeclaringEntityType; Assert.Same(ordersView, orderDetailsType.GetViewMappings().Single().View); Assert.Equal( ordersView.GetReferencingRowInternalForeignKeys(orderType), ordersView.GetRowInternalForeignKeys(orderDetailsType)); + Assert.Equal( + RelationalStrings.TableNotMappedEntityType(nameof(SpecialCustomer), ordersView.Name), + Assert.Throws( + () => ordersView.GetReferencingRowInternalForeignKeys(specialCustomerType)).Message); + Assert.Equal( + RelationalStrings.TableNotMappedEntityType(nameof(SpecialCustomer), ordersView.Name), + Assert.Throws( + () => ordersView.GetRowInternalForeignKeys(specialCustomerType)).Message); Assert.False(ordersView.IsOptional(orderType)); Assert.True(ordersView.IsOptional(orderDetailsType)); - - var orderDetailsDate = orderDetailsType.FindProperty(nameof(OrderDetails.OrderDate)); + Assert.Equal( + RelationalStrings.TableNotMappedEntityType(nameof(SpecialCustomer), ordersView.Name), + Assert.Throws( + () => ordersView.IsOptional(specialCustomerType)).Message); var orderDateColumn = orderDateMapping.Column; Assert.Same(orderDateColumn, ordersView.FindColumn("OrderDate")); Assert.Same(orderDateColumn, orderDate.FindColumn(StoreObjectIdentifier.View(ordersView.Name, ordersView.Schema))); Assert.Same(orderDateColumn, ordersView.FindColumn(orderDate)); + + var orderDetailsDate = orderDetailsType.FindProperty(nameof(OrderDetails.OrderDate)); Assert.Equal(new[] { orderDate, orderDetailsDate }, orderDateColumn.PropertyMappings.Select(m => m.Property)); Assert.Equal("OrderDate", orderDateColumn.Name); Assert.Equal("default_datetime_mapping", orderDateColumn.StoreType); Assert.False(orderDateColumn.IsNullable); Assert.Same(ordersView, orderDateColumn.Table); - var customerType = model.Model.FindEntityType(typeof(Customer)); - var customerView = customerType.GetViewMappings().Single().View; - Assert.Equal("CustomerView", customerView.Name); - Assert.Equal("viewSchema", customerView.Schema); - - var specialCustomerType = model.Model.FindEntityType(typeof(SpecialCustomer)); - var extraSpecialCustomerType = model.Model.FindEntityType(typeof(ExtraSpecialCustomer)); - var customerPk = specialCustomerType.FindPrimaryKey(); - + var customerView = customerType.GetViewMappings().Last().View; Assert.False(customerView.IsOptional(customerType)); - Assert.False(customerView.IsOptional(specialCustomerType)); - Assert.False(customerView.IsOptional(extraSpecialCustomerType)); + if (mapping == Mapping.TPC) + { + Assert.Equal( + RelationalStrings.TableNotMappedEntityType(nameof(SpecialCustomer), customerView.Name), + Assert.Throws( + () => customerView.IsOptional(specialCustomerType)).Message); + } + else + { + Assert.False(customerView.IsOptional(specialCustomerType)); + Assert.False(customerView.IsOptional(extraSpecialCustomerType)); + } - var mappedToTable = orderType.GetTableName() != null; + var baseTableName = mapping == Mapping.TPH + ? abstractBaseType.GetTableName() + : customerType.GetTableName(); + var mappedToTable = baseTableName != null; var ordersCustomerForeignKey = orderType.FindNavigation(nameof(Order.Customer)).ForeignKey; - Assert.Equal(mappedToTable - ? "FK_Order_Customer_CustomerId" + Assert.Equal(mappedToTable && mapping != Mapping.TPC + ? "FK_Order_" + baseTableName + "_CustomerId" : null, ordersCustomerForeignKey.GetConstraintName()); Assert.Null(ordersCustomerForeignKey.GetConstraintName( StoreObjectIdentifier.View(ordersView.Name, ordersView.Schema), StoreObjectIdentifier.View(customerView.Name, customerView.Schema))); - Assert.Equal(mappedToTable - ? "FK_Order_Customer_CustomerId" + Assert.Equal(mappedToTable && mapping != Mapping.TPC + ? "FK_Order_" + baseTableName + "_CustomerId" : null, ordersCustomerForeignKey.GetDefaultName()); Assert.Null(ordersCustomerForeignKey.GetDefaultName( StoreObjectIdentifier.View(ordersView.Name, ordersView.Schema), @@ -265,7 +370,9 @@ private static void AssertViews(IRelationalModel model, Mapping mapping) if (mapping == Mapping.TPT) { - Assert.Equal(2, specialCustomerType.GetViewMappings().Count()); + Assert.Equal("CustomerView", customerView.Name); + Assert.Equal("viewSchema", customerView.Schema); + Assert.Equal(3, specialCustomerType.GetViewMappings().Count()); Assert.True(specialCustomerType.GetViewMappings().First().IsSplitEntityTypePrincipal); Assert.False(specialCustomerType.GetViewMappings().First().IncludesDerivedTypes); Assert.True(specialCustomerType.GetViewMappings().Last().IsSplitEntityTypePrincipal); @@ -274,7 +381,7 @@ private static void AssertViews(IRelationalModel model, Mapping mapping) var specialCustomerView = specialCustomerType.GetViewMappings().Select(t => t.Table) .First(t => t.Name == "SpecialCustomerView"); Assert.Null(specialCustomerView.Schema); - Assert.Equal(5, specialCustomerView.Columns.Count()); + Assert.Equal(6, specialCustomerView.Columns.Count()); Assert.True(specialCustomerView.EntityTypeMappings.Single(m => m.EntityType == specialCustomerType).IsSharedTablePrincipal); @@ -290,17 +397,40 @@ private static void AssertViews(IRelationalModel model, Mapping mapping) { var specialCustomerViewMapping = specialCustomerType.GetViewMappings().Single(); Assert.True(specialCustomerViewMapping.IsSplitEntityTypePrincipal); - Assert.True(specialCustomerViewMapping.IncludesDerivedTypes); - var specialCustomerView = specialCustomerViewMapping.View; - Assert.Same(customerView, specialCustomerView); + var specialityColumn = specialCustomerView.Columns.Single(c => c.Name == nameof(SpecialCustomer.Speciality)); + if (mapping == Mapping.TPH) + { + var baseView = abstractBaseType.GetViewMappings().Single().Table; + Assert.Equal("BaseView", baseView.Name); + Assert.Equal(baseView.Name, abstractBaseType.GetViewName()); + Assert.Equal(baseView.Name, customerView.Name); + Assert.Equal(baseView.Schema, customerView.Schema); + Assert.True(specialCustomerViewMapping.IncludesDerivedTypes); + Assert.Same(customerView, specialCustomerView); + + Assert.Equal(6, specialCustomerView.EntityTypeMappings.Count()); + Assert.True(specialCustomerView.EntityTypeMappings.First().IsSharedTablePrincipal); + Assert.False(specialCustomerView.EntityTypeMappings.Last().IsSharedTablePrincipal); + + Assert.True(specialityColumn.IsNullable); + } + else + { + Assert.False(specialCustomerViewMapping.IncludesDerivedTypes); + Assert.NotSame(customerView, specialCustomerView); - Assert.Equal(4, specialCustomerView.EntityTypeMappings.Count()); - Assert.True(specialCustomerView.EntityTypeMappings.First().IsSharedTablePrincipal); - Assert.False(specialCustomerView.EntityTypeMappings.Last().IsSharedTablePrincipal); + Assert.True(customerView.EntityTypeMappings.Single().IsSharedTablePrincipal); + Assert.Equal(5, customerView.Columns.Count()); - var specialityColumn = specialCustomerView.Columns.Single(c => c.Name == nameof(SpecialCustomer.Speciality)); - Assert.True(specialityColumn.IsNullable); + Assert.Equal(2, specialCustomerView.EntityTypeMappings.Count()); + Assert.True(specialCustomerView.EntityTypeMappings.First().IsSharedTablePrincipal); + Assert.False(specialCustomerView.EntityTypeMappings.Last().IsSharedTablePrincipal); + + Assert.Equal(10, specialCustomerView.Columns.Count()); + + Assert.False(specialityColumn.IsNullable); + } } } @@ -405,17 +535,9 @@ private static void AssertTables(IRelationalModel model, Mapping mapping) Assert.Equal("DateDetails", orderDateFkConstraint.PrincipalTable.Name); var orderCustomerFk = orderType.GetForeignKeys().Single(fk => fk.PrincipalEntityType.ClrType == typeof(Customer)); - var orderCustomerFkConstraint = orderCustomerFk.GetMappedConstraints().Single(); - - Assert.Equal("FK_Order_Customer_CustomerId", orderCustomerFkConstraint.Name); - Assert.Equal(nameof(Order.CustomerId), orderCustomerFkConstraint.Columns.Single().Name); - Assert.Equal(nameof(Customer.Id), orderCustomerFkConstraint.PrincipalColumns.Single().Name); - Assert.Same(ordersTable, orderCustomerFkConstraint.Table); - Assert.Equal("Customer", orderCustomerFkConstraint.PrincipalTable.Name); - Assert.Equal(ReferentialAction.Cascade, orderCustomerFkConstraint.OnDeleteAction); - Assert.Equal(orderCustomerFk, orderCustomerFkConstraint.MappedForeignKeys.Single()); - Assert.Equal(new[] { orderDateFkConstraint, orderCustomerFkConstraint }, ordersTable.ForeignKeyConstraints); + var abstractBaseType = model.Model.FindEntityType(typeof(AbstractBase)); + var abstractCustomerType = model.Model.FindEntityType(typeof(AbstractCustomer)); var customerType = model.Model.FindEntityType(typeof(Customer)); var specialCustomerType = model.Model.FindEntityType(typeof(SpecialCustomer)); var extraSpecialCustomerType = model.Model.FindEntityType(typeof(ExtraSpecialCustomer)); @@ -477,20 +599,7 @@ private static void AssertTables(IRelationalModel model, Mapping mapping) Assert.Equal("FK_DateDetails", orderDateFkConstraint.Name); - var customerTable = customerType.GetTableMappings().Single().Table; - Assert.Equal("Customer", customerTable.Name); - - var ordersCustomerForeignKey = orderType.FindNavigation(nameof(Order.Customer)).ForeignKey; - Assert.Equal("FK_Order_Customer_CustomerId", ordersCustomerForeignKey.GetConstraintName()); - Assert.Equal("FK_Order_Customer_CustomerId", ordersCustomerForeignKey.GetConstraintName( - StoreObjectIdentifier.Table(ordersTable.Name, ordersTable.Schema), - StoreObjectIdentifier.Table(customerTable.Name, customerTable.Schema))); - Assert.Equal("FK_Order_Customer_CustomerId", ordersCustomerForeignKey.GetDefaultName()); - Assert.Equal("FK_Order_Customer_CustomerId", ordersCustomerForeignKey.GetDefaultName( - StoreObjectIdentifier.Table(ordersTable.Name, ordersTable.Schema), - StoreObjectIdentifier.Table(customerTable.Name, customerTable.Schema))); - - var ordersCustomerIndex = orderType.FindIndex(ordersCustomerForeignKey.Properties); + var ordersCustomerIndex = orderType.FindIndex(orderCustomerFk.Properties); Assert.Equal("IX_Order_CustomerId", ordersCustomerIndex.GetDatabaseName()); Assert.Equal("IX_Order_CustomerId", ordersCustomerIndex.GetDatabaseName( StoreObjectIdentifier.Table(ordersTable.Name, ordersTable.Schema))); @@ -515,23 +624,38 @@ private static void AssertTables(IRelationalModel model, Mapping mapping) Assert.Equal("Speciality", specialityCK.GetDefaultName( StoreObjectIdentifier.Table(specialCustomerTable.Name, specialCustomerTable.Schema))); + var customerTable = customerType.GetTableMappings().Last().Table; Assert.False(customerTable.IsOptional(customerType)); - Assert.False(customerTable.IsOptional(specialCustomerType)); - Assert.False(customerTable.IsOptional(extraSpecialCustomerType)); + if (mapping == Mapping.TPC) + { + Assert.Equal( + RelationalStrings.TableNotMappedEntityType(nameof(SpecialCustomer), customerTable.Name), + Assert.Throws( + () => customerTable.IsOptional(specialCustomerType)).Message); + } + else + { + Assert.False(customerTable.IsOptional(specialCustomerType)); + Assert.False(customerTable.IsOptional(extraSpecialCustomerType)); + } var customerPk = specialCustomerType.FindPrimaryKey(); if (mapping == Mapping.TPT) { - Assert.Equal(2, specialCustomerType.GetTableMappings().Count()); + var baseTable = abstractBaseType.GetTableMappings().Single().Table; + Assert.Equal("AbstractBase", baseTable.Name); + Assert.Equal(nameof(Customer), customerTable.Name); + Assert.Null(abstractCustomerType.GetTableName()); + Assert.Equal(nameof(SpecialCustomer), specialCustomerType.GetTableName()); + Assert.Equal(3, specialCustomerType.GetTableMappings().Count()); Assert.True(specialCustomerType.GetTableMappings().First().IsSplitEntityTypePrincipal); Assert.False(specialCustomerType.GetTableMappings().First().IncludesDerivedTypes); Assert.True(specialCustomerType.GetTableMappings().Last().IsSplitEntityTypePrincipal); Assert.True(specialCustomerType.GetTableMappings().Last().IncludesDerivedTypes); Assert.Equal("SpecialCustomer", specialCustomerTable.Name); - Assert.Equal("SpecialSchema", specialCustomerTable.Schema); - Assert.Equal(5, specialCustomerTable.Columns.Count()); + Assert.Equal(6, specialCustomerTable.Columns.Count()); Assert.True( specialCustomerTable.EntityTypeMappings.Single(m => m.EntityType == specialCustomerType).IsSharedTablePrincipal); @@ -539,53 +663,80 @@ private static void AssertTables(IRelationalModel model, Mapping mapping) var specialityColumn = specialCustomerTable.Columns.Single(c => c.Name == nameof(SpecialCustomer.Speciality)); Assert.False(specialityColumn.IsNullable); - var addressColumn = specialCustomerTable.Columns.Single( - c => c.Name == nameof(SpecialCustomer.Details) + "_" + nameof(CustomerDetails.Address)); + var addressColumn = specialCustomerTable.Columns.Single(c => + c.Name == nameof(SpecialCustomer.Details) + "_" + nameof(CustomerDetails.Address)); Assert.False(addressColumn.IsNullable); var specialityProperty = specialityColumn.PropertyMappings.First().Property; Assert.Equal( RelationalStrings.PropertyNotMappedToTable( nameof(SpecialCustomer.Speciality), nameof(SpecialCustomer), "Customer"), - Assert.Throws( - () => specialityProperty.IsColumnNullable(StoreObjectIdentifier.Table(customerTable.Name, customerTable.Schema))) + Assert.Throws(() => + specialityProperty.IsColumnNullable(StoreObjectIdentifier.Table(customerTable.Name, customerTable.Schema))) .Message); + var abstractStringColumn = specialCustomerTable.Columns.Single(c => c.Name == nameof(AbstractCustomer.AbstractString)); + Assert.False(specialityColumn.IsNullable); + Assert.Equal(2, specialityColumn.PropertyMappings.Count()); + var extraSpecialCustomerTable = extraSpecialCustomerType.GetTableMappings().Select(t => t.Table).First(t => t.Name == "ExtraSpecialCustomer"); Assert.Empty(customerTable.CheckConstraints); Assert.Same(specialityCK, specialCustomerTable.CheckConstraints.Single()); - Assert.Same(specialityCK, extraSpecialCustomerTable.CheckConstraints.Single()); + Assert.Empty(extraSpecialCustomerTable.CheckConstraints); - Assert.Equal(3, customerPk.GetMappedConstraints().Count()); + Assert.Equal(4, customerPk.GetMappedConstraints().Count()); var specialCustomerPkConstraint = specialCustomerTable.PrimaryKey; Assert.Equal("PK_SpecialCustomer", specialCustomerPkConstraint.Name); Assert.Same(specialCustomerPkConstraint.MappedKeys.First(), customerPk); var idProperty = customerPk.Properties.Single(); - Assert.Equal(6, idProperty.GetTableColumnMappings().Count()); - - Assert.Empty(customerTable.ForeignKeyConstraints); - - var specialCustomerUniqueConstraint = customerTable.UniqueConstraints.Single(c => !c.GetIsPrimaryKey()); - Assert.Equal("AK_Customer_SpecialityAk", specialCustomerUniqueConstraint.Name); + Assert.Equal(10, idProperty.GetTableColumnMappings().Count()); + + var customerFk = customerTable.ForeignKeyConstraints.Single(); + Assert.Equal("FK_Customer_AbstractBase_Id", customerFk.Name); + Assert.NotNull(customerFk.MappedForeignKeys.Single()); + Assert.Same(baseTable, customerFk.PrincipalTable); + + var orderCustomerFkConstraint = orderCustomerFk.GetMappedConstraints().Single(); + + Assert.Equal("FK_Order_Customer_CustomerId", orderCustomerFkConstraint.Name); + Assert.Equal(nameof(Order.CustomerId), orderCustomerFkConstraint.Columns.Single().Name); + Assert.Equal(nameof(Customer.Id), orderCustomerFkConstraint.PrincipalColumns.Single().Name); + Assert.Same(ordersTable, orderCustomerFkConstraint.Table); + Assert.Equal("Customer", orderCustomerFkConstraint.PrincipalTable.Name); + Assert.Equal(ReferentialAction.Cascade, orderCustomerFkConstraint.OnDeleteAction); + Assert.Equal(orderCustomerFk, orderCustomerFkConstraint.MappedForeignKeys.Single()); + Assert.Equal(new[] { orderDateFkConstraint, orderCustomerFkConstraint }, ordersTable.ForeignKeyConstraints); + + Assert.Equal(orderCustomerFkConstraint.Name, orderCustomerFk.GetConstraintName()); + Assert.Equal(orderCustomerFkConstraint.Name, orderCustomerFk.GetConstraintName( + StoreObjectIdentifier.Table(ordersTable.Name, ordersTable.Schema), + StoreObjectIdentifier.Table(customerTable.Name, customerTable.Schema))); + Assert.Equal(orderCustomerFkConstraint.Name, orderCustomerFk.GetDefaultName()); + Assert.Equal(orderCustomerFkConstraint.Name, orderCustomerFk.GetDefaultName( + StoreObjectIdentifier.Table(ordersTable.Name, ordersTable.Schema), + StoreObjectIdentifier.Table(customerTable.Name, customerTable.Schema))); + + var specialCustomerUniqueConstraint = baseTable.UniqueConstraints.Single(c => !c.GetIsPrimaryKey()); + Assert.Equal("AK_AbstractBase_SpecialityAk", specialCustomerUniqueConstraint.Name); Assert.NotNull(specialCustomerUniqueConstraint.MappedKeys.Single()); var foreignKeys = specialCustomerTable.ForeignKeyConstraints.ToArray(); Assert.Equal(3, foreignKeys.Length); - var specialCustomerTptFkConstraint = foreignKeys[0]; + var specialCustomerFkConstraint = foreignKeys[0]; + Assert.Equal("FK_SpecialCustomer_AbstractBase_RelatedCustomerSpeciality", specialCustomerFkConstraint.Name); + Assert.NotNull(specialCustomerFkConstraint.MappedForeignKeys.Single()); + Assert.Same(baseTable, specialCustomerFkConstraint.PrincipalTable); + + var specialCustomerTptFkConstraint = foreignKeys[1]; Assert.Equal("FK_SpecialCustomer_Customer_Id", specialCustomerTptFkConstraint.Name); Assert.NotNull(specialCustomerTptFkConstraint.MappedForeignKeys.Single()); Assert.Same(customerTable, specialCustomerTptFkConstraint.PrincipalTable); - var specialCustomerFkConstraint = foreignKeys[1]; - Assert.Equal("FK_SpecialCustomer_Customer_RelatedCustomerSpeciality", specialCustomerFkConstraint.Name); - Assert.NotNull(specialCustomerFkConstraint.MappedForeignKeys.Single()); - Assert.Same(customerTable, specialCustomerFkConstraint.PrincipalTable); - var anotherSpecialCustomerFkConstraint = foreignKeys[2]; Assert.Equal("FK_SpecialCustomer_SpecialCustomer_AnotherRelatedCustomerId", anotherSpecialCustomerFkConstraint.Name); Assert.NotNull(anotherSpecialCustomerFkConstraint.MappedForeignKeys.Single()); @@ -608,48 +759,125 @@ private static void AssertTables(IRelationalModel model, Mapping mapping) { var specialCustomerTypeMapping = specialCustomerType.GetTableMappings().Single(); Assert.True(specialCustomerTypeMapping.IsSplitEntityTypePrincipal); - Assert.True(specialCustomerTypeMapping.IncludesDerivedTypes); - - Assert.Same(customerTable, specialCustomerTable); - - Assert.Equal(4, specialCustomerTable.EntityTypeMappings.Count()); - Assert.True(specialCustomerTable.EntityTypeMappings.First().IsSharedTablePrincipal); - Assert.False(specialCustomerTable.EntityTypeMappings.Last().IsSharedTablePrincipal); var specialityColumn = specialCustomerTable.Columns.Single(c => c.Name == nameof(SpecialCustomer.Speciality)); - Assert.True(specialityColumn.IsNullable); - var addressColumn = specialCustomerTable.Columns.Single(c => - c.Name == nameof(SpecialCustomer.Details) + "_" + nameof(CustomerDetails.Address)); - Assert.True(addressColumn.IsNullable); - - Assert.Same(specialityCK, specialCustomerTable.CheckConstraints.Single()); + c.Name == nameof(SpecialCustomer.Details) + "_" + nameof(CustomerDetails.Address)); var specialCustomerPkConstraint = specialCustomerTable.PrimaryKey; - Assert.Equal("PK_Customer", specialCustomerPkConstraint.Name); - Assert.Same(specialCustomerPkConstraint.MappedKeys.First(), customerPk); + var specialCustomerUniqueConstraint = specialCustomerTable.UniqueConstraints.Single(c => !c.GetIsPrimaryKey()); + var specialCustomerDbIndex = specialCustomerTable.Indexes.Last(); + var anotherSpecialCustomerDbIndex = specialCustomerTable.Indexes.First(); var idProperty = customerPk.Properties.Single(); - Assert.Equal(3, idProperty.GetTableColumnMappings().Count()); - var specialCustomerUniqueConstraint = specialCustomerTable.UniqueConstraints.Single(c => !c.GetIsPrimaryKey()); - Assert.Equal("AK_Customer_SpecialityAk", specialCustomerUniqueConstraint.Name); - Assert.NotNull(specialCustomerUniqueConstraint.MappedKeys.Single()); + if (mapping == Mapping.TPH) + { + var baseTable = abstractBaseType.GetTableMappings().Single().Table; + Assert.Equal("AbstractBase", baseTable.Name); + Assert.Equal(baseTable.Name, abstractBaseType.GetTableName()); + Assert.Equal(baseTable.Name, customerTable.Name); + Assert.Equal(baseTable.Name, abstractCustomerType.GetTableName()); + Assert.Equal(baseTable.Name, specialCustomerType.GetTableName()); + + Assert.True(specialCustomerTypeMapping.IncludesDerivedTypes); + Assert.Same(customerTable, specialCustomerTable); + + Assert.Equal(6, specialCustomerTable.EntityTypeMappings.Count()); + Assert.True(specialCustomerTable.EntityTypeMappings.First().IsSharedTablePrincipal); + Assert.False(specialCustomerTable.EntityTypeMappings.Last().IsSharedTablePrincipal); + + Assert.Equal(11, specialCustomerTable.Columns.Count()); + + Assert.True(specialityColumn.IsNullable); + Assert.True(addressColumn.IsNullable); + + var orderCustomerFkConstraint = orderCustomerFk.GetMappedConstraints().Single(); + + Assert.Equal("FK_Order_" + baseTable.Name + "_CustomerId", orderCustomerFkConstraint.Name); + Assert.Equal(nameof(Order.CustomerId), orderCustomerFkConstraint.Columns.Single().Name); + Assert.Equal(nameof(Customer.Id), orderCustomerFkConstraint.PrincipalColumns.Single().Name); + Assert.Same(ordersTable, orderCustomerFkConstraint.Table); + Assert.Equal(baseTable.Name, orderCustomerFkConstraint.PrincipalTable.Name); + Assert.Equal(ReferentialAction.Cascade, orderCustomerFkConstraint.OnDeleteAction); + Assert.Equal(orderCustomerFk, orderCustomerFkConstraint.MappedForeignKeys.Single()); + Assert.Equal(new[] { orderDateFkConstraint, orderCustomerFkConstraint }, ordersTable.ForeignKeyConstraints); + + Assert.Equal(orderCustomerFkConstraint.Name, orderCustomerFk.GetConstraintName()); + Assert.Equal(orderCustomerFkConstraint.Name, orderCustomerFk.GetConstraintName( + StoreObjectIdentifier.Table(ordersTable.Name, ordersTable.Schema), + StoreObjectIdentifier.Table(customerTable.Name, customerTable.Schema))); + Assert.Equal(orderCustomerFkConstraint.Name, orderCustomerFk.GetDefaultName()); + Assert.Equal(orderCustomerFkConstraint.Name, orderCustomerFk.GetDefaultName( + StoreObjectIdentifier.Table(ordersTable.Name, ordersTable.Schema), + StoreObjectIdentifier.Table(customerTable.Name, customerTable.Schema))); + + Assert.Equal("PK_" + baseTable.Name, specialCustomerPkConstraint.Name); + Assert.Equal("AK_AbstractBase_SpecialityAk", specialCustomerUniqueConstraint.Name); + + var specialCustomerFkConstraint = specialCustomerTable.ForeignKeyConstraints.Last(); + Assert.Equal("FK_AbstractBase_AbstractBase_RelatedCustomerSpeciality", specialCustomerFkConstraint.Name); + Assert.NotNull(specialCustomerFkConstraint.MappedForeignKeys.Single()); + + var anotherSpecialCustomerFkConstraint = specialCustomerTable.ForeignKeyConstraints.First(); + Assert.Equal("FK_AbstractBase_AbstractBase_AnotherRelatedCustomerId", anotherSpecialCustomerFkConstraint.Name); + Assert.NotNull(anotherSpecialCustomerFkConstraint.MappedForeignKeys.Single()); + + Assert.Equal("IX_AbstractBase_RelatedCustomerSpeciality", specialCustomerDbIndex.Name); + Assert.Equal("IX_AbstractBase_AnotherRelatedCustomerId", anotherSpecialCustomerDbIndex.Name); + + Assert.Equal(5, idProperty.GetTableColumnMappings().Count()); + } + else + { + Assert.Null(abstractBaseType.GetTableName()); + Assert.Equal(nameof(Customer), customerTable.Name); + Assert.Null(abstractCustomerType.GetTableName()); + Assert.Equal(nameof(SpecialCustomer), specialCustomerType.GetTableName()); - var specialCustomerFkConstraint = specialCustomerTable.ForeignKeyConstraints.Last(); - Assert.Equal("FK_Customer_Customer_RelatedCustomerSpeciality", specialCustomerFkConstraint.Name); - Assert.NotNull(specialCustomerFkConstraint.MappedForeignKeys.Single()); + Assert.False(specialCustomerTypeMapping.IncludesDerivedTypes); + Assert.NotSame(customerTable, specialCustomerTable); - var anotherSpecialCustomerFkConstraint = specialCustomerTable.ForeignKeyConstraints.First(); - Assert.Equal("FK_Customer_Customer_AnotherRelatedCustomerId", anotherSpecialCustomerFkConstraint.Name); - Assert.NotNull(anotherSpecialCustomerFkConstraint.MappedForeignKeys.Single()); + Assert.True(customerTable.EntityTypeMappings.Single().IsSharedTablePrincipal); + Assert.Equal(5, customerTable.Columns.Count()); - var specialCustomerDbIndex = specialCustomerTable.Indexes.Last(); - Assert.Equal("IX_Customer_RelatedCustomerSpeciality", specialCustomerDbIndex.Name); - Assert.NotNull(specialCustomerDbIndex.MappedIndexes.Single()); + Assert.Equal(2, specialCustomerTable.EntityTypeMappings.Count()); + Assert.True(specialCustomerTable.EntityTypeMappings.First().IsSharedTablePrincipal); + Assert.False(specialCustomerTable.EntityTypeMappings.Last().IsSharedTablePrincipal); + + Assert.Equal(10, specialCustomerTable.Columns.Count()); + + Assert.False(specialityColumn.IsNullable); + Assert.False(addressColumn.IsNullable); + + // Derived principal entity types are mapped to different tables, so the constraint is not enforceable + Assert.Empty(orderCustomerFk.GetMappedConstraints()); + + Assert.Null(orderCustomerFk.GetConstraintName()); + Assert.Null(orderCustomerFk.GetConstraintName( + StoreObjectIdentifier.Table(ordersTable.Name, ordersTable.Schema), + StoreObjectIdentifier.Table(customerTable.Name, customerTable.Schema))); + Assert.Null(orderCustomerFk.GetDefaultName()); + Assert.Null(orderCustomerFk.GetDefaultName( + StoreObjectIdentifier.Table(ordersTable.Name, ordersTable.Schema), + StoreObjectIdentifier.Table(customerTable.Name, customerTable.Schema))); + + Assert.Equal("PK_SpecialCustomer", specialCustomerPkConstraint.Name); + Assert.Equal("AK_SpecialCustomer_SpecialityAk", specialCustomerUniqueConstraint.Name); + + Assert.Empty(specialCustomerTable.ForeignKeyConstraints); + + Assert.Equal("IX_SpecialCustomer_RelatedCustomerSpeciality", specialCustomerDbIndex.Name); + Assert.Equal("IX_SpecialCustomer_AnotherRelatedCustomerId", anotherSpecialCustomerDbIndex.Name); + + Assert.Equal(3, idProperty.GetTableColumnMappings().Count()); + } + + Assert.Same(specialCustomerPkConstraint.MappedKeys.First(), customerPk); + + Assert.NotNull(specialCustomerUniqueConstraint.MappedKeys.Single()); - var anotherSpecialCustomerDbIndex = specialCustomerTable.Indexes.First(); - Assert.Equal("IX_Customer_AnotherRelatedCustomerId", anotherSpecialCustomerDbIndex.Name); + Assert.NotNull(specialCustomerDbIndex.MappedIndexes.Single()); Assert.NotNull(specialCustomerDbIndex.MappedIndexes.Single()); } } @@ -657,37 +885,79 @@ private static void AssertTables(IRelationalModel model, Mapping mapping) private IRelationalModel CreateTestModel(bool mapToTables = false, bool mapToViews = false, Mapping mapping = Mapping.TPH) { var modelBuilder = CreateConventionModelBuilder(); - modelBuilder.Entity( + + modelBuilder.Entity( cb => { - if (mapToViews) + if (mapping != Mapping.TPC) { - cb.ToView("CustomerView", "viewSchema"); + if (mapToViews) + { + cb.ToView("BaseView", "viewSchema"); + } + + if (mapToTables) + { + cb.ToTable("AbstractBase"); + } } - if (mapToTables) + if (mapping == Mapping.TPC) { - cb.ToTable("Customer"); + cb.UseTpcMappingStrategy(); + } + else if (mapping == Mapping.TPT + && (!mapToTables && !mapToViews)) + { + cb.UseTptMappingStrategy(); } + // TODO: Don't map it on the base #19811 cb.Property("SpecialityAk"); }); - modelBuilder.Entity( + modelBuilder.Entity( cb => { - if (mapToViews - && mapping == Mapping.TPT) + if (mapping != Mapping.TPH) { - cb.ToView("SpecialCustomerView"); + if (mapToViews) + { + cb.ToView("CustomerView", "viewSchema"); + } + + if (mapToTables) + { + cb.ToTable("Customer"); + } } + }); - if (mapToTables - && mapping == Mapping.TPT) + modelBuilder.Entity( + cb => + { + if (mapping == Mapping.TPT) { - cb.ToTable("SpecialCustomer", "SpecialSchema"); + cb.ToView(null); + cb.ToTable((string)null); } + }); + modelBuilder.Entity( + cb => + { + if (mapping != Mapping.TPH) + { + if (mapToViews) + { + cb.ToView("SpecialCustomerView"); + } + + if (mapToTables) + { + cb.ToTable("SpecialCustomer", "SpecialSchema"); + } + } cb.HasCheckConstraint($"Speciality", $"[Speciality] IN ('Specialist', 'Generalist')"); cb.Property(s => s.Speciality).IsRequired(); @@ -706,16 +976,17 @@ private IRelationalModel CreateTestModel(bool mapToTables = false, bool mapToVie modelBuilder.Entity( cb => { - if (mapToViews - && mapping == Mapping.TPT) + if (mapping != Mapping.TPH) { - cb.ToView("ExtraSpecialCustomerView"); - } + if (mapToViews) + { + cb.ToView("ExtraSpecialCustomerView"); + } - if (mapToTables - && mapping == Mapping.TPT) - { - cb.ToTable("ExtraSpecialCustomer", "ExtraSpecialSchema"); + if (mapToTables) + { + cb.ToTable("ExtraSpecialCustomer", "ExtraSpecialSchema"); + } } }); @@ -1028,9 +1299,11 @@ protected virtual TestHelpers.TestModelBuilder CreateConventionModelBuilder() public enum Mapping { +#pragma warning disable SA1602 // Enumeration items should be documented TPH, TPT, TPC +#pragma warning restore SA1602 // Enumeration items should be documented } private enum MyEnum : ulong @@ -1040,9 +1313,13 @@ private enum MyEnum : ulong Tue } - private class Customer + private abstract class AbstractBase { public int Id { get; set; } + } + + private class Customer : AbstractBase + { public string Name { get; set; } public short SomeShort { get; set; } public MyEnum EnumValue { get; set; } @@ -1050,7 +1327,14 @@ private class Customer public IEnumerable Orders { get; set; } } - private class SpecialCustomer : Customer +#nullable enable + private abstract class AbstractCustomer : Customer + { + public string AbstractString { get; set; } = null!; + } +#nullable disable + + private class SpecialCustomer : AbstractCustomer { public string Speciality { get; set; } public string RelatedCustomerSpeciality { get; set; } diff --git a/test/EFCore.Relational.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs b/test/EFCore.Relational.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs index 51ce5387750..1e600d513fe 100644 --- a/test/EFCore.Relational.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs +++ b/test/EFCore.Relational.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs @@ -109,6 +109,57 @@ public static ModelBuilderTest.TestPropertyBuilder IsFixedLength UseTpcMappingStrategy( + this ModelBuilderTest.TestEntityTypeBuilder builder) + where TEntity : class + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.UseTpcMappingStrategy(); + break; + case IInfrastructure nongenericBuilder: + nongenericBuilder.Instance.UseTpcMappingStrategy(); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestEntityTypeBuilder UseTphMappingStrategy( + this ModelBuilderTest.TestEntityTypeBuilder builder) + where TEntity : class + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.UseTphMappingStrategy(); + break; + case IInfrastructure nongenericBuilder: + nongenericBuilder.Instance.UseTphMappingStrategy(); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestEntityTypeBuilder UseTptMappingStrategy( + this ModelBuilderTest.TestEntityTypeBuilder builder) + where TEntity : class + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.UseTptMappingStrategy(); + break; + case IInfrastructure nongenericBuilder: + nongenericBuilder.Instance.UseTptMappingStrategy(); + break; + } + + return builder; + } + public static ModelBuilderTest.TestEntityTypeBuilder ToTable( this ModelBuilderTest.TestEntityTypeBuilder builder, string? name) diff --git a/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderGenericTest.cs b/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderGenericTest.cs index d0fe859cf83..a16912db9b5 100644 --- a/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderGenericTest.cs +++ b/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderGenericTest.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. - - // ReSharper disable InconsistentNaming namespace Microsoft.EntityFrameworkCore.ModelBuilding; @@ -244,7 +242,27 @@ public void Index_convention_sets_filter_for_unique_index_when_base_type_changed } [ConditionalFact] - public virtual void TPT_identifying_FK_are_created_only_on_declaring_type() + public virtual void Can_override_TPC_with_TPH() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity

(); + modelBuilder.Entity(); + modelBuilder.Entity(); + modelBuilder.Entity() + .UseTpcMappingStrategy() + .UseTphMappingStrategy(); + + var model = modelBuilder.FinalizeModel(); + + Assert.Equal("Discriminator", model.FindEntityType(typeof(PBase)).GetDiscriminatorPropertyName()); + Assert.Equal(nameof(PBase), model.FindEntityType(typeof(PBase)).GetDiscriminatorValue()); + Assert.Equal(nameof(P), model.FindEntityType(typeof(P)).GetDiscriminatorValue()); + Assert.Equal(nameof(Q), model.FindEntityType(typeof(Q)).GetDiscriminatorValue()); + } + + [ConditionalFact] + public virtual void TPT_identifying_FK_is_created_only_on_declaring_table() { var modelBuilder = CreateModelBuilder(); modelBuilder.Entity() @@ -273,11 +291,13 @@ public virtual void TPT_identifying_FK_are_created_only_on_declaring_type() var principalType = model.FindEntityType(typeof(BigMak)); Assert.Empty(principalType.GetForeignKeys()); Assert.Empty(principalType.GetIndexes()); + Assert.Null(principalType.FindDiscriminatorProperty()); var ingredientType = model.FindEntityType(typeof(Ingredient)); var bunType = model.FindEntityType(typeof(Bun)); Assert.Empty(bunType.GetIndexes()); + Assert.Null(bunType.FindDiscriminatorProperty()); var bunFk = bunType.GetDeclaredForeignKeys().Single(fk => !fk.IsBaseLinking()); Assert.Equal("FK_Buns_BigMak_Id", bunFk.GetConstraintName()); Assert.Equal( @@ -306,6 +326,64 @@ public virtual void TPT_identifying_FK_are_created_only_on_declaring_type() Assert.Single(sesameBunFk.GetMappedConstraints()); } + [ConditionalFact] + public virtual void TPC_identifying_FKs_are_created_on_all_tables() + { + var modelBuilder = CreateModelBuilder(); + modelBuilder.Entity() + .Ignore(b => b.Bun) + .Ignore(b => b.Pickles); + modelBuilder.Entity( + b => + { + b.ToTable("Ingredients"); + b.Ignore(i => i.BigMak); + b.UseTpcMappingStrategy(); + }); + modelBuilder.Entity( + b => + { + b.ToTable("Buns"); + b.HasOne(i => i.BigMak).WithOne().HasForeignKey(i => i.Id); + b.UseTpcMappingStrategy(); + }); + modelBuilder.Entity( + b => + { + b.ToTable("SesameBuns"); + }); + + var model = modelBuilder.FinalizeModel(); + + var principalType = model.FindEntityType(typeof(BigMak)); + Assert.Empty(principalType.GetForeignKeys()); + Assert.Empty(principalType.GetIndexes()); + Assert.Null(principalType.FindDiscriminatorProperty()); + + var ingredientType = model.FindEntityType(typeof(Ingredient)); + + var bunType = model.FindEntityType(typeof(Bun)); + Assert.Empty(bunType.GetIndexes()); + Assert.Null(bunType.FindDiscriminatorProperty()); + var bunFk = bunType.GetDeclaredForeignKeys().Single(); + Assert.Equal("FK_Buns_BigMak_Id", bunFk.GetConstraintName()); + Assert.Equal( + "FK_Buns_BigMak_Id", bunFk.GetConstraintName( + StoreObjectIdentifier.Create(bunType, StoreObjectType.Table).Value, + StoreObjectIdentifier.Create(principalType, StoreObjectType.Table).Value)); + Assert.Equal(2, bunFk.GetMappedConstraints().Count()); + + Assert.Empty(bunType.GetDeclaredForeignKeys().Where(fk => fk.IsBaseLinking())); + + var sesameBunType = model.FindEntityType(typeof(SesameBun)); + Assert.Empty(sesameBunType.GetIndexes()); + Assert.Empty(sesameBunType.GetDeclaredForeignKeys()); + Assert.Equal( + "FK_SesameBuns_BigMak_Id", bunFk.GetConstraintName( + StoreObjectIdentifier.Create(sesameBunType, StoreObjectType.Table).Value, + StoreObjectIdentifier.Create(principalType, StoreObjectType.Table).Value)); + } + [ConditionalFact] public virtual void TPT_index_can_use_inherited_properties() {