diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index 98f1c51809f..634aae7f1df 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -845,8 +845,8 @@ private void GenerateTableMapping( var table = StoreObjectIdentifier.Create(entityType, StoreObjectType.Table); var tableName = (string?)tableNameAnnotation?.Value ?? table?.Name; if (tableNameAnnotation == null - && (tableName == null - || entityType.BaseType?.GetTableName() == tableName)) + && entityType.BaseType != null + && entityType.BaseType.GetTableName() == tableName) { return; } diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs index 853202cc686..17375d35a1b 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs @@ -19,6 +19,9 @@ public static partial 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. /// + /// + /// See Entity type hierarchy mapping for more information and examples. + /// /// 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) @@ -32,6 +35,9 @@ public static EntityTypeBuilder UseTpcMappingStrategy(this EntityTypeBuilder ent /// 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. /// + /// + /// See Entity type hierarchy mapping for more information and examples. + /// /// 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) @@ -45,6 +51,9 @@ public static EntityTypeBuilder UseTphMappingStrategy(this EntityTypeBuilder ent /// 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 Entity type hierarchy mapping for more information and examples. + /// /// 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) @@ -58,6 +67,9 @@ public static EntityTypeBuilder UseTptMappingStrategy(this EntityTypeBuilder ent /// 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. /// + /// + /// See Entity type hierarchy mapping for more information and examples. + /// /// 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) @@ -68,6 +80,9 @@ public static EntityTypeBuilder UseTpcMappingStrategy(this Ent /// 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. /// + /// + /// See Entity type hierarchy mapping for more information and examples. + /// /// 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) @@ -78,12 +93,60 @@ public static EntityTypeBuilder UseTphMappingStrategy(this Ent /// 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 Entity type hierarchy mapping for more information and examples. + /// /// 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(); + /// + /// Sets the hierarchy mapping strategy. + /// + /// + /// See Entity type hierarchy mapping for more information and examples. + /// + /// The builder for the entity type being configured. + /// The mapping strategy for the derived types. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + public static IConventionEntityTypeBuilder? UseMappingStrategy( + this IConventionEntityTypeBuilder entityTypeBuilder, + string? strategy, + bool fromDataAnnotation = false) + { + if (!entityTypeBuilder.CanSetMappingStrategy(strategy, fromDataAnnotation)) + { + return null; + } + + entityTypeBuilder.Metadata.SetMappingStrategy(strategy, fromDataAnnotation); + return entityTypeBuilder; + } + + /// + /// Returns a value indicating whether the hierarchy mapping strategy can be configured + /// using the specified configuration source. + /// + /// + /// See Database migrations for more information and examples. + /// + /// The builder for the entity type being configured. + /// The mapping strategy for the derived types. + /// Indicates whether the configuration was specified using a data annotation. + /// if the configuration can be applied. + public static bool CanSetMappingStrategy( + this IConventionEntityTypeBuilder entityTypeBuilder, + string? strategy, + bool fromDataAnnotation = false) + => entityTypeBuilder.CanSetAnnotation + (RelationalAnnotationNames.MappingStrategy, strategy, fromDataAnnotation); + /// /// Mark the table that this entity type is mapped to as excluded from migrations. /// diff --git a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs index e14f264d4ad..40d5ae86af7 100644 --- a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs @@ -1451,7 +1451,7 @@ public static IEnumerable GetOverrides(thi /// The property. /// The property facet overrides. public static IEnumerable GetOverrides(this IMutableProperty property) - => ((IEnumerable?)RelationalPropertyOverrides.Get(property)) + => RelationalPropertyOverrides.Get(property)?.Cast() ?? Enumerable.Empty(); /// @@ -1466,7 +1466,7 @@ public static IEnumerable GetOverrides(this /// The property. /// The property facet overrides. public static IEnumerable GetOverrides(this IConventionProperty property) - => ((IEnumerable?)RelationalPropertyOverrides.Get(property)) + => RelationalPropertyOverrides.Get(property)?.Cast() ?? Enumerable.Empty(); /// @@ -1481,7 +1481,7 @@ public static IEnumerable GetOverrides(t /// The property. /// The property facet overrides. public static IEnumerable GetOverrides(this IProperty property) - => ((IEnumerable?)RelationalPropertyOverrides.Get(property)) + => RelationalPropertyOverrides.Get(property)?.Cast() ?? Enumerable.Empty(); /// diff --git a/src/EFCore.Relational/Metadata/Conventions/EntityTypeHierarchyMappingConvention.cs b/src/EFCore.Relational/Metadata/Conventions/EntityTypeHierarchyMappingConvention.cs index 0e5d5ef3eb4..0b5b3c92df4 100644 --- a/src/EFCore.Relational/Metadata/Conventions/EntityTypeHierarchyMappingConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/EntityTypeHierarchyMappingConvention.cs @@ -40,6 +40,7 @@ public virtual void ProcessModelFinalizing( IConventionModelBuilder modelBuilder, IConventionContext context) { + var allRoots = new HashSet(); var nonTphRoots = new HashSet(); foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) @@ -49,15 +50,24 @@ public virtual void ProcessModelFinalizing( continue; } + var root = entityType.GetRootType(); + allRoots.Add(root); var mappingStrategy = (string?)entityType[RelationalAnnotationNames.MappingStrategy]; if (mappingStrategy == null) { - mappingStrategy = (string?)entityType.GetRootType()[RelationalAnnotationNames.MappingStrategy]; + mappingStrategy = (string?)root[RelationalAnnotationNames.MappingStrategy]; + if (mappingStrategy == null + && root.GetDiscriminatorPropertyConfigurationSource() == ConfigurationSource.Explicit) + { + mappingStrategy = RelationalAnnotationNames.TphMappingStrategy; + root.Builder.UseMappingStrategy(RelationalAnnotationNames.TphMappingStrategy); + continue; + } } if (mappingStrategy == RelationalAnnotationNames.TpcMappingStrategy) { - nonTphRoots.Add(entityType.GetRootType()); + nonTphRoots.Add(root); continue; } @@ -70,6 +80,7 @@ public virtual void ProcessModelFinalizing( || entityType.GetSchema() != entityType.BaseType.GetSchema()) { mappingStrategy = RelationalAnnotationNames.TptMappingStrategy; + root.Builder.UseMappingStrategy(mappingStrategy); } } @@ -96,7 +107,7 @@ public virtual void ProcessModelFinalizing( } } - nonTphRoots.Add(entityType.GetRootType()); + nonTphRoots.Add(root); continue; } } @@ -106,13 +117,20 @@ public virtual void ProcessModelFinalizing( && (viewName != entityType.BaseType.GetViewName() || entityType.GetViewSchema() != entityType.BaseType.GetViewSchema())) { - nonTphRoots.Add(entityType.GetRootType()); + nonTphRoots.Add(root); + continue; } } foreach (var root in nonTphRoots) { + allRoots.Remove(root); root.Builder.HasNoDiscriminator(); } + + foreach (var root in allRoots) + { + root.Builder.UseMappingStrategy(RelationalAnnotationNames.TphMappingStrategy); + } } } diff --git a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs index 33d2dfcb622..e3812030b50 100644 --- a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs +++ b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs @@ -83,6 +83,8 @@ public override ConventionSet CreateConventionSet() conventionSet.EntityTypeBaseTypeChangedConventions.Add(checkConstraintConvention); conventionSet.EntityTypeBaseTypeChangedConventions.Add(triggerConvention); + conventionSet.EntityTypeAnnotationChangedConventions.Add(tableNameFromDbSetConvention); + ReplaceConvention(conventionSet.ForeignKeyPropertiesChangedConventions, valueGenerationConvention); ReplaceConvention(conventionSet.ForeignKeyOwnershipChangedConventions, valueGenerationConvention); diff --git a/src/EFCore.Relational/Metadata/Conventions/TableNameFromDbSetConvention.cs b/src/EFCore.Relational/Metadata/Conventions/TableNameFromDbSetConvention.cs index b063958ca3a..6c740d98fbd 100644 --- a/src/EFCore.Relational/Metadata/Conventions/TableNameFromDbSetConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/TableNameFromDbSetConvention.cs @@ -12,6 +12,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; public class TableNameFromDbSetConvention : IEntityTypeAddedConvention, IEntityTypeBaseTypeChangedConvention, + IEntityTypeAnnotationChangedConvention, IModelFinalizingConvention { private readonly IDictionary _sets; @@ -63,13 +64,7 @@ public TableNameFromDbSetConvention( /// protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; } - /// - /// Called after the base type of an entity type changes. - /// - /// The builder for the entity type. - /// The new base entity type. - /// The old base entity type. - /// Additional information associated with convention execution. + /// public virtual void ProcessEntityTypeBaseTypeChanged( IConventionEntityTypeBuilder entityTypeBuilder, IConventionEntityType? newBaseType, @@ -79,7 +74,9 @@ public virtual void ProcessEntityTypeBaseTypeChanged( var entityType = entityTypeBuilder.Metadata; if (oldBaseType == null - && newBaseType != null) + && newBaseType != null + && (entityType.GetMappingStrategy() ?? RelationalAnnotationNames.TphMappingStrategy) + == RelationalAnnotationNames.TphMappingStrategy) { entityTypeBuilder.HasNoAnnotation(RelationalAnnotationNames.TableName); } @@ -92,24 +89,46 @@ public virtual void ProcessEntityTypeBaseTypeChanged( } } - /// - /// Called after an entity type is added to the model. - /// - /// The builder for the entity type. - /// Additional information associated with convention execution. + /// public virtual void ProcessEntityTypeAdded( IConventionEntityTypeBuilder entityTypeBuilder, IConventionContext context) { var entityType = entityTypeBuilder.Metadata; - if (entityType.BaseType == null - && !entityType.HasSharedClrType + if (!entityType.HasSharedClrType + && (entityType.BaseType == null + || (entityType.GetMappingStrategy() ?? RelationalAnnotationNames.TphMappingStrategy) + != RelationalAnnotationNames.TphMappingStrategy) && _sets.TryGetValue(entityType.ClrType, out var setName)) { entityTypeBuilder.ToTable(setName); } } + /// + public virtual void ProcessEntityTypeAnnotationChanged( + IConventionEntityTypeBuilder entityTypeBuilder, + string name, + IConventionAnnotation? annotation, + IConventionAnnotation? oldAnnotation, + IConventionContext context) + { + if (name == RelationalAnnotationNames.MappingStrategy + && annotation != null + && (entityTypeBuilder.Metadata.GetMappingStrategy() ?? RelationalAnnotationNames.TphMappingStrategy) + != RelationalAnnotationNames.TphMappingStrategy) + { + foreach (var deriverEntityType in entityTypeBuilder.Metadata.GetDerivedTypesInclusive()) + { + if (!deriverEntityType.HasSharedClrType + && _sets.TryGetValue(deriverEntityType.ClrType, out var setName)) + { + deriverEntityType.Builder.ToTable(setName); + } + } + } + } + /// public virtual void ProcessModelFinalizing( IConventionModelBuilder modelBuilder, diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs index d507f8515a6..09cb794579e 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs @@ -18,6 +18,7 @@ public class CSharpMigrationsGeneratorTest { private static readonly string _nl = Environment.NewLine; private static readonly string _toTable = _nl + @"entityTypeBuilder.ToTable(""WithAnnotations"")"; + private static readonly string _toNullTable = _nl + @"entityTypeBuilder.ToTable((string)null)"; [ConditionalFact] public void Test_new_annotations_handled_for_entity_types() @@ -139,19 +140,22 @@ public void Test_new_annotations_handled_for_entity_types() #pragma warning disable CS0612 // Type or member is obsolete CoreAnnotationNames.DefiningQuery, #pragma warning restore CS0612 // Type or member is obsolete - (Expression.Lambda(Expression.Constant(null)), "") + (Expression.Lambda(Expression.Constant(null)), _toNullTable) }, { RelationalAnnotationNames.ViewName, - ("MyView", _nl + "entityTypeBuilder." + nameof(RelationalEntityTypeBuilderExtensions.ToView) + @"(""MyView"")") + ("MyView", _toNullTable + ";" + _nl + _nl + + "entityTypeBuilder." + nameof(RelationalEntityTypeBuilderExtensions.ToView) + @"(""MyView"")") }, { RelationalAnnotationNames.FunctionName, - (null, _nl + "entityTypeBuilder." + nameof(RelationalEntityTypeBuilderExtensions.ToFunction) + @"(null)") + (null, _toNullTable + ";" + _nl + _nl + + "entityTypeBuilder." + nameof(RelationalEntityTypeBuilderExtensions.ToFunction) + @"(null)") }, { RelationalAnnotationNames.SqlQuery, - (null, _nl + "entityTypeBuilder." + nameof(RelationalEntityTypeBuilderExtensions.ToSqlQuery) + @"(null)") + (null, _toNullTable + ";" + _nl + _nl + + "entityTypeBuilder." + nameof(RelationalEntityTypeBuilderExtensions.ToSqlQuery) + @"(null)") } }; diff --git a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs index 3645ca93e4f..109664c73ec 100644 --- a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs +++ b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs @@ -215,10 +215,13 @@ public enum TestEnum Value2 } - private class BaseEntity + private abstract class AbstractBase { public int Id { get; set; } - + } + + private class BaseEntity : AbstractBase + { public string Discriminator { get; set; } } @@ -455,11 +458,12 @@ public virtual void Entities_are_stored_in_model_snapshot_for_TPT() builder.Entity() .ToTable("DerivedEntity", "foo"); builder.Entity(); + builder.Entity(); }, AddBoilerPlate( GetHeading() + @" - modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+BaseEntity"", b => + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+AbstractBase"", b => { b.Property(""Id"") .ValueGeneratedOnAdd() @@ -467,11 +471,17 @@ public virtual void Entities_are_stored_in_model_snapshot_for_TPT() SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property(""Id""), 1L, 1); - b.Property(""Discriminator"") - .HasColumnType(""nvarchar(max)""); - b.HasKey(""Id""); + b.ToTable(""AbstractBase""); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+BaseEntity"", b => + { + b.HasBaseType(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+AbstractBase""); + b.ToTable(""BaseEntity""); }); @@ -493,14 +503,21 @@ public virtual void Entities_are_stored_in_model_snapshot_for_TPT() .OnDelete(DeleteBehavior.ClientCascade) .IsRequired(); });"), - o => + model => { - Assert.Equal(4, o.GetAnnotations().Count()); + Assert.Equal(4, model.GetAnnotations().Count()); + Assert.Equal(3, model.GetEntityTypes().Count()); - Assert.Equal( - "DerivedEntity", - o.FindEntityType("Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+DerivedEntity") - .GetTableName()); + var abstractBase = model.FindEntityType("Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+AbstractBase"); + Assert.Equal("AbstractBase", abstractBase.GetTableName()); + Assert.Equal("TPT", abstractBase.GetMappingStrategy()); + + var baseType = model.FindEntityType("Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+BaseEntity"); + Assert.Equal("BaseEntity", baseType.GetTableName()); + + var derived = model.FindEntityType("Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+DerivedEntity"); + Assert.Equal("DerivedEntity", derived.GetTableName()); + Assert.Equal("foo", derived.GetSchema()); }); [ConditionalFact] @@ -529,6 +546,8 @@ public virtual void Entities_are_stored_in_model_snapshot_for_TPT_with_one_exclu b.HasKey(""Id""); b.ToTable(""BaseEntity""); + + b.UseTptMappingStrategy(); }); modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+DerivedEntity"", b => @@ -576,6 +595,8 @@ public void Views_are_stored_in_the_model_snapshot() b.HasKey(""Id""); + b.ToTable((string)null); + b.ToView(""EntityWithOneProperty"", (string)null); });"), o => Assert.Equal("EntityWithOneProperty", o.GetEntityTypes().Single().GetViewName())); @@ -596,6 +617,8 @@ public void Views_with_schemas_are_stored_in_the_model_snapshot() b.HasKey(""Id""); + b.ToTable((string)null); + b.ToView(""EntityWithOneProperty"", ""ViewSchema""); });"), o => @@ -612,12 +635,13 @@ public virtual void Entities_are_stored_in_model_snapshot_for_TPC() builder.Entity() .ToTable("DerivedEntity", "foo") .ToView("DerivedView", "foo"); - builder.Entity().UseTpcMappingStrategy(); + builder.Entity(); + builder.Entity().UseTpcMappingStrategy(); }, AddBoilerPlate( GetHeading() + @" - modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+BaseEntity"", b => + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+AbstractBase"", b => { b.Property(""Id"") .ValueGeneratedOnAdd() @@ -625,16 +649,20 @@ public virtual void Entities_are_stored_in_model_snapshot_for_TPC() SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property(""Id""), 1L, 1); - b.Property(""Discriminator"") - .HasColumnType(""nvarchar(max)""); - b.HasKey(""Id""); - b.ToTable(""BaseEntity""); + b.ToTable((string)null); b.UseTpcMappingStrategy(); }); + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+BaseEntity"", b => + { + b.HasBaseType(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+AbstractBase""); + + b.ToTable(""BaseEntity""); + }); + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+DerivedEntity"", b => { b.HasBaseType(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+BaseEntity""); @@ -646,11 +674,21 @@ public virtual void Entities_are_stored_in_model_snapshot_for_TPC() b.ToView(""DerivedView"", ""foo""); });"), - o => + model => { - Assert.Equal(4, o.GetAnnotations().Count()); + Assert.Equal(4, model.GetAnnotations().Count()); + Assert.Equal(3, model.GetEntityTypes().Count()); + + var abstractBase = model.FindEntityType("Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+AbstractBase"); + Assert.Null(abstractBase.GetTableName()); + Assert.Null(abstractBase.GetViewName()); + Assert.Equal("TPC", abstractBase.GetMappingStrategy()); - var derived = o.FindEntityType("Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+DerivedEntity"); + var baseType = model.FindEntityType("Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+BaseEntity"); + Assert.Equal("BaseEntity", baseType.GetTableName()); + Assert.Null(baseType.GetViewName()); + + var derived = model.FindEntityType("Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+DerivedEntity"); Assert.Equal("DerivedEntity", derived.GetTableName()); Assert.Equal("DerivedView", derived.GetViewName()); }); @@ -979,6 +1017,8 @@ public virtual void Entity_splitting_is_stored_in_snapshot_with_views() b.HasKey(""Id""); + b.ToTable((string)null); + b.ToView(""EntityWithOneProperty"", null, v => { v.Property(""Shadow""); @@ -1002,6 +1042,8 @@ public virtual void Entity_splitting_is_stored_in_snapshot_with_views() b1.HasKey(""Id""); + b1.ToTable((string)null); + b1.ToView(""EntityWithOneProperty"", null, v => { v.Property(""AlternateId"") @@ -1132,6 +1174,8 @@ public void Entity_types_mapped_to_functions_are_stored_in_the_model_snapshot() b.Property(""Something"") .HasColumnType(""nvarchar(max)""); + b.ToTable((string)null); + b.ToFunction(""GetCount""); });"), o => Assert.Equal("GetCount", o.GetEntityTypes().Single().GetFunctionName())); @@ -1150,6 +1194,8 @@ public void Entity_types_mapped_to_queries_are_stored_in_the_model_snapshot() b.HasKey(""Id""); + b.ToTable((string)null); + b.ToSqlQuery(""query""); });"), o => Assert.Equal("query", o.GetEntityTypes().Single().GetSqlQuery())); @@ -1264,6 +1310,8 @@ public virtual void CheckConstraint_is_only_stored_in_snapshot_once_for_TPH() b.ToTable(""BaseEntity""); b.HasDiscriminator(""Discriminator"").HasValue(""BaseEntity""); + + b.UseTphMappingStrategy(); }); modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+DerivedEntity"", b => @@ -1567,6 +1615,8 @@ public virtual void BaseType_is_stored_in_snapshot() b.ToTable(""BaseEntity""); b.HasDiscriminator(""Discriminator"").HasValue(""BaseEntity""); + + b.UseTphMappingStrategy(); }); modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+AnotherDerivedEntity"", b => @@ -1634,6 +1684,8 @@ public virtual void Discriminator_annotations_are_stored_in_snapshot() b.ToTable(""BaseEntity""); b.HasDiscriminator(""Discriminator"").IsComplete(true).HasValue(""BaseEntity""); + + b.UseTphMappingStrategy(); }); modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+AnotherDerivedEntity"", b => @@ -1707,6 +1759,8 @@ public virtual void Converted_discriminator_annotations_are_stored_in_snapshot() b.ToTable(""BaseEntityWithStructDiscriminator""); b.HasDiscriminator(""Discriminator"").IsComplete(true).HasValue(""Base""); + + b.UseTphMappingStrategy(); }); modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+AnotherDerivedEntityWithStructDiscriminator"", b => @@ -3368,6 +3422,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.HasKey(""TestOwnerId"", ""Id""); + b1.ToTable((string)null); + b1.ToView(""OwnedView"", (string)null); b1.WithOwner() @@ -5810,6 +5866,8 @@ public virtual void Do_not_generate_entity_type_builder_again_if_no_foreign_key_ b.ToTable(""BaseType""); b.HasDiscriminator(""Discriminator"").HasValue(""BaseType""); + + b.UseTphMappingStrategy(); }); modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithOneProperty"", b => diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs index e7bb3a7ec0a..ded382af523 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs @@ -1048,6 +1048,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) { runtimeEntityType.AddAnnotation(""DiscriminatorMappingComplete"", false); runtimeEntityType.AddAnnotation(""Relational:FunctionName"", null); + runtimeEntityType.AddAnnotation(""Relational:MappingStrategy"", ""TPH""); runtimeEntityType.AddAnnotation(""Relational:Schema"", null); runtimeEntityType.AddAnnotation(""Relational:SqlQuery"", null); runtimeEntityType.AddAnnotation(""Relational:TableName"", ""PrincipalDerived""); @@ -1166,6 +1167,7 @@ public static RuntimeSkipNavigation CreateSkipNavigation1(RuntimeEntityType decl public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) { runtimeEntityType.AddAnnotation(""Relational:FunctionName"", null); + runtimeEntityType.AddAnnotation(""Relational:MappingStrategy"", ""TPT""); runtimeEntityType.AddAnnotation(""Relational:Schema"", ""mySchema""); runtimeEntityType.AddAnnotation(""Relational:SqlQuery"", null); runtimeEntityType.AddAnnotation(""Relational:TableName"", ""PrincipalBase""); diff --git a/test/EFCore.Relational.Specification.Tests/EntitySplittingTestBase.cs b/test/EFCore.Relational.Specification.Tests/EntitySplittingTestBase.cs index 8bb4e5698c6..7adb3cc2777 100644 --- a/test/EFCore.Relational.Specification.Tests/EntitySplittingTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/EntitySplittingTestBase.cs @@ -63,15 +63,21 @@ protected virtual void OnModelCreating(ModelBuilder modelBuilder) }); } - protected async Task InitializeAsync(Action onModelCreating, bool sensitiveLogEnabled = true) - => ContextFactory = await InitializeAsync( + protected async Task InitializeAsync( + Action onModelCreating, + Action onConfiguring = null, + Action seed = null, + bool sensitiveLogEnabled = true) + => ContextFactory = await InitializeAsync( onModelCreating, + seed: seed, shouldLogCategory: _ => true, onConfiguring: options => { options.ConfigureWarnings(w => w.Log(RelationalEventId.OptionalDependentWithAllNullPropertiesWarning)) .ConfigureWarnings(w => w.Log(RelationalEventId.OptionalDependentWithoutIdentifyingPropertyWarning)) .EnableSensitiveDataLogging(sensitiveLogEnabled); + onConfiguring?.Invoke(options); } ); diff --git a/test/EFCore.Relational.Specification.Tests/Query/TPCGearsOfWarQueryRelationalFixture.cs b/test/EFCore.Relational.Specification.Tests/Query/TPCGearsOfWarQueryRelationalFixture.cs index f05df02116b..fd03855e562 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/TPCGearsOfWarQueryRelationalFixture.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/TPCGearsOfWarQueryRelationalFixture.cs @@ -20,8 +20,7 @@ protected override bool ShouldLogCategory(string logCategory) public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) => base.AddOptions(builder).ConfigureWarnings( - w => - w.Log(RelationalEventId.ForeignKeyTpcPrincipalWarning)); + w => w.Log(RelationalEventId.ForeignKeyTpcPrincipalWarning)); protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) { @@ -31,12 +30,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con modelBuilder.Entity().UseTpcMappingStrategy(); modelBuilder.Entity().UseTpcMappingStrategy(); - modelBuilder.Entity().ToTable("Gears"); - modelBuilder.Entity().ToTable("Officers"); - modelBuilder.Entity().ToTable("LocustHordes"); - modelBuilder.Entity().ToTable("LocustLeaders"); modelBuilder.Entity().ToTable("LocustCommanders"); } } diff --git a/test/EFCore.Relational.Specification.Tests/Query/TPCInheritanceQueryFixture.cs b/test/EFCore.Relational.Specification.Tests/Query/TPCInheritanceQueryFixture.cs index a2139b79b18..333f775354f 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/TPCInheritanceQueryFixture.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/TPCInheritanceQueryFixture.cs @@ -20,7 +20,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con { base.OnModelCreating(modelBuilder, context); - // Configure TPT for hierarchies + // Configure TPC for hierarchies modelBuilder.Entity().UseTpcMappingStrategy(); modelBuilder.Entity().UseTpcMappingStrategy(); modelBuilder.Entity().UseTpcMappingStrategy(); @@ -31,12 +31,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever(); modelBuilder.Entity().ToTable("Birds"); - modelBuilder.Entity().ToTable("Eagle"); modelBuilder.Entity().ToTable("Kiwi"); modelBuilder.Entity().Property(e => e.Species).HasMaxLength(100); modelBuilder.Entity().HasMany(e => e.Prey).WithOne().HasForeignKey(e => e.EagleId).IsRequired(false); - modelBuilder.Entity().ToTable("Drinks"); modelBuilder.Entity().ToTable("Coke"); modelBuilder.Entity().ToTable("Lilt"); modelBuilder.Entity().ToTable("Tea"); diff --git a/test/EFCore.Relational.Specification.Tests/Query/TPTGearsOfWarQueryRelationalFixture.cs b/test/EFCore.Relational.Specification.Tests/Query/TPTGearsOfWarQueryRelationalFixture.cs index 02a0a44096c..b6a64a0ea02 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/TPTGearsOfWarQueryRelationalFixture.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/TPTGearsOfWarQueryRelationalFixture.cs @@ -22,13 +22,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con { base.OnModelCreating(modelBuilder, context); - modelBuilder.Entity().ToTable("Gears"); - modelBuilder.Entity().ToTable("Officers"); + modelBuilder.Entity().UseTptMappingStrategy(); - modelBuilder.Entity().ToTable("Factions"); modelBuilder.Entity().ToTable("LocustHordes"); - modelBuilder.Entity().ToTable("LocustLeaders"); modelBuilder.Entity().ToTable("LocustCommanders"); } } diff --git a/test/EFCore.Relational.Specification.Tests/Query/TPTInheritanceQueryFixture.cs b/test/EFCore.Relational.Specification.Tests/Query/TPTInheritanceQueryFixture.cs index 117da367aa7..385a84d0c61 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/TPTInheritanceQueryFixture.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/TPTInheritanceQueryFixture.cs @@ -20,20 +20,16 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con { base.OnModelCreating(modelBuilder, context); - modelBuilder.Entity().ToTable("Plants"); modelBuilder.Entity().ToTable("Flowers"); modelBuilder.Entity().ToTable("Roses"); modelBuilder.Entity().ToTable("Daisies"); modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever(); - modelBuilder.Entity().ToTable("Animals"); modelBuilder.Entity().ToTable("Birds"); - modelBuilder.Entity().ToTable("Eagle"); modelBuilder.Entity().ToTable("Kiwi"); modelBuilder.Entity().Property(e => e.Species).HasMaxLength(100); modelBuilder.Entity().HasMany(e => e.Prey).WithOne().HasForeignKey(e => e.EagleId).IsRequired(false); - modelBuilder.Entity().ToTable("Drinks"); modelBuilder.Entity().ToTable("Coke"); modelBuilder.Entity().ToTable("Lilt"); modelBuilder.Entity().ToTable("Tea"); diff --git a/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/GearsOfWarContext.cs b/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/GearsOfWarContext.cs index 07b412e130c..906c9a67340 100644 --- a/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/GearsOfWarContext.cs +++ b/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/GearsOfWarContext.cs @@ -11,6 +11,7 @@ public GearsOfWarContext(DbContextOptions options) } public DbSet Gears { get; set; } + public DbSet Officers { get; set; } public DbSet Squads { get; set; } public DbSet Tags { get; set; } public DbSet Weapons { get; set; } diff --git a/test/EFCore.SqlServer.FunctionalTests/EntitySplittingSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/EntitySplittingSqlServerTest.cs index 881e49d40ea..73bbaf2ddd5 100644 --- a/test/EFCore.SqlServer.FunctionalTests/EntitySplittingSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/EntitySplittingSqlServerTest.cs @@ -9,12 +9,59 @@ public EntitySplittingSqlServerTest(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { } - - protected override ITestStoreFactory TestStoreFactory - => SqlServerTestStoreFactory.Instance; - protected override void OnModelCreating(ModelBuilder modelBuilder) + [ConditionalFact(Skip = "Entity splitting query Issue #620")] + public virtual async Task Can_roundtrip_with_triggers() { - base.OnModelCreating(modelBuilder); + await InitializeAsync(modelBuilder => + { + OnModelCreating(modelBuilder); + + modelBuilder.Entity( + ob => + { + ob.SplitToTable( + "MeterReadingDetails", t => + { + t.HasTrigger("MeterReadingsDetails_Trigger"); + }); + }); + }, + sensitiveLogEnabled: false, + seed: c => + { + c.Database.ExecuteSqlRaw($@" +CREATE OR ALTER TRIGGER [MeterReadingsDetails_Trigger] +ON [MeterReadingDetails] +FOR INSERT, UPDATE, DELETE AS +BEGIN + IF @@ROWCOUNT = 0 + return +END"); + }); + + await using (var context = CreateContext()) + { + var meterReading = new MeterReading { ReadingStatus = MeterReadingStatus.NotAccesible, CurrentRead = "100" }; + + context.Add(meterReading); + + TestSqlLoggerFactory.Clear(); + + await context.SaveChangesAsync(); + + Assert.Empty(TestSqlLoggerFactory.Log.Where(l => l.Level == LogLevel.Warning)); + } + + await using (var context = CreateContext()) + { + var reading = await context.MeterReadings.SingleAsync(); + + Assert.Equal(MeterReadingStatus.NotAccesible, reading.ReadingStatus); + Assert.Equal("100", reading.CurrentRead); + } } + + protected override ITestStoreFactory TestStoreFactory + => SqlServerTestStoreFactory.Instance; } diff --git a/test/EFCore.SqlServer.FunctionalTests/SqlServerTriggersTest.cs b/test/EFCore.SqlServer.FunctionalTests/SqlServerTriggersTest.cs index 344e5b10b1a..ba68ce68328 100644 --- a/test/EFCore.SqlServer.FunctionalTests/SqlServerTriggersTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/SqlServerTriggersTest.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 ParameterOnlyUsedForPreconditionCheck.Local // ReSharper disable InconsistentNaming namespace Microsoft.EntityFrameworkCore; diff --git a/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerTestModelBuilderExtensions.cs b/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerTestModelBuilderExtensions.cs index 030428c099a..68826a9ad65 100644 --- a/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerTestModelBuilderExtensions.cs +++ b/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerTestModelBuilderExtensions.cs @@ -64,18 +64,18 @@ public static RelationalModelBuilderTest.TestTableBuilder IsTemporal IsTemporal( this RelationalModelBuilderTest.TestTableBuilder builder, - Action> buildAction) + Action> buildAction) where TEntity : class { switch (builder) { case IInfrastructure> genericBuilder: genericBuilder.Instance.IsTemporal( - b => buildAction(new SqlServerModelBuilderGenericTest.GenericTestTemporalTableBuilder(b))); + b => buildAction(new SqlServerModelBuilderTestBase.GenericTestTemporalTableBuilder(b))); break; case IInfrastructure nongenericBuilder: nongenericBuilder.Instance.IsTemporal( - b => buildAction(new SqlServerModelBuilderGenericTest.NonGenericTestTemporalTableBuilder(b))); + b => buildAction(new SqlServerModelBuilderTestBase.NonGenericTestTemporalTableBuilder(b))); break; }