From d2b6759f1ea204314952c6aafd53be306cf3c56d Mon Sep 17 00:00:00 2001 From: AndriySvyryd Date: Tue, 23 Jun 2020 11:20:44 -0700 Subject: [PATCH] Allow to exclude a table from migrations Store the entity types mapped to views in the snapshot Fixes #2725 --- .../Internal/CosmosTypeMappingSource.cs | 1 + .../Design/CSharpSnapshotGenerator.cs | 87 +++++--- .../RelationalEntityTypeBuilderExtensions.cs | 186 +++++++++++++++++- .../RelationalEntityTypeExtensions.cs | 52 +++-- .../RelationalModelValidator.cs | 12 +- .../Metadata/Internal/RelationalModel.cs | 11 +- .../Metadata/RelationalAnnotationNames.cs | 5 + .../Properties/RelationalStrings.Designer.cs | 8 + .../Properties/RelationalStrings.resx | 3 + .../Design/CSharpMigrationsGeneratorTest.cs | 2 +- .../Migrations/ModelSnapshotSqlServerTest.cs | 71 ++++++- .../RelationalModelValidatorTest.cs | 51 ++++- .../RelationalApiConsistencyTest.cs | 3 +- .../SqlServerModelBuilderGenericTest.cs | 2 +- 14 files changed, 433 insertions(+), 61 deletions(-) diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs index b4d86e46fe1..8ace7c1eab1 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Utilities; +using Newtonsoft.Json.Linq; namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal { diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index 7542807cea0..2938f9c1373 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -88,10 +88,10 @@ public virtual void Generate(string builderName, IModel model, IndentedStringBui GenerateSequence(builderName, sequence, stringBuilder); } - GenerateEntityTypes(builderName, Sort(model.GetEntityTypes().Where(et => !et.IsIgnoredByMigrations()).ToList()), stringBuilder); + GenerateEntityTypes(builderName, Sort(model.GetEntityTypes()), stringBuilder); } - private static IReadOnlyList Sort(IReadOnlyList entityTypes) + private static IReadOnlyList Sort(IEnumerable entityTypes) { var entityTypeGraph = new Multigraph(); entityTypeGraph.AddVertices(entityTypes); @@ -774,38 +774,75 @@ protected virtual void GenerateEntityTypeAnnotations( .ToDictionary(a => a.Name, a => a); var tableNameAnnotation = annotations.Find(RelationalAnnotationNames.TableName); - var schemaAnnotation = annotations.Find(RelationalAnnotationNames.Schema); - - var nonDefaultName = false; if (tableNameAnnotation?.Value != null || entityType.BaseType == null) { - stringBuilder - .AppendLine() - .Append(builderName) - .Append(".") - .Append(nameof(RelationalEntityTypeBuilderExtensions.ToTable)) - .Append("(") - .Append(Code.Literal((string)tableNameAnnotation?.Value ?? entityType.GetTableName())); - if (tableNameAnnotation != null) + var tableName = (string)tableNameAnnotation?.Value ?? entityType.GetTableName(); + if (tableName != null) { - annotations.Remove(tableNameAnnotation.Name); + stringBuilder + .AppendLine() + .Append(builderName) + .Append(".ToTable(") + .Append(Code.Literal(tableName)); + if (tableNameAnnotation != null) + { + annotations.Remove(tableNameAnnotation.Name); + } + + var schemaAnnotation = annotations.Find(RelationalAnnotationNames.Schema); + if (schemaAnnotation?.Value != null) + { + stringBuilder + .Append(", ") + .Append(Code.Literal((string)schemaAnnotation.Value)); + annotations.Remove(schemaAnnotation.Name); + } + + var isExcludedAnnotation = annotations.Find(RelationalAnnotationNames.IsTableExcludedFromMigrations); + if (isExcludedAnnotation != null) + { + if (((bool?)isExcludedAnnotation.Value) == true) + { + stringBuilder + .Append(", ") + .Append(Code.Literal(true)); + } + annotations.Remove(isExcludedAnnotation.Name); + } + + stringBuilder.AppendLine(");"); } - nonDefaultName = true; } - if (schemaAnnotation?.Value != null) + var viewNameAnnotation = annotations.Find(RelationalAnnotationNames.ViewName); + if (viewNameAnnotation?.Value != null + || entityType.BaseType == null) { - stringBuilder - .Append(",") - .Append(Code.Literal((string)schemaAnnotation.Value)); - annotations.Remove(schemaAnnotation.Name); - nonDefaultName = true; - } + var viewName = (string)viewNameAnnotation?.Value ?? entityType.GetViewName(); + if (viewName != null) + { + stringBuilder + .AppendLine() + .Append(builderName) + .Append(".ToView(") + .Append(Code.Literal(viewName)); + if (viewNameAnnotation != null) + { + annotations.Remove(viewNameAnnotation.Name); + } - if (nonDefaultName) - { - stringBuilder.AppendLine(");"); + var viewSchemaAnnotation = annotations.Find(RelationalAnnotationNames.ViewSchema); + if (viewSchemaAnnotation?.Value != null) + { + stringBuilder + .Append(", ") + .Append(Code.Literal((string)viewSchemaAnnotation.Value)); + annotations.Remove(viewSchemaAnnotation.Name); + } + + stringBuilder.AppendLine(");"); + } } if ((discriminatorPropertyAnnotation?.Value diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs index 26224a7d2cf..c00ccd6c0de 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs @@ -24,13 +24,26 @@ public static class RelationalEntityTypeBuilderExtensions public static EntityTypeBuilder ToTable( [NotNull] this EntityTypeBuilder entityTypeBuilder, [CanBeNull] string name) + => ToTable(entityTypeBuilder, name, excludedFromMigrations: false); + + /// + /// Configures the table that the entity type maps to when targeting a relational database. + /// + /// The builder for the entity type being configured. + /// The name of the table. + /// A value indicating whether the table should be managed by migrations. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder ToTable( + [NotNull] this EntityTypeBuilder entityTypeBuilder, + [CanBeNull] string name, + bool excludedFromMigrations) { Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); Check.NullButNotEmpty(name, nameof(name)); entityTypeBuilder.Metadata.SetTableName(name); entityTypeBuilder.Metadata.SetSchema(null); - entityTypeBuilder.Metadata.RemoveAnnotation(RelationalAnnotationNames.ViewDefinitionSql); + entityTypeBuilder.Metadata.SetIsTableExcludedFromMigrations(excludedFromMigrations); return entityTypeBuilder; } @@ -46,7 +59,22 @@ public static EntityTypeBuilder ToTable( [NotNull] this EntityTypeBuilder entityTypeBuilder, [CanBeNull] string name) where TEntity : class - => (EntityTypeBuilder)ToTable((EntityTypeBuilder)entityTypeBuilder, name); + => (EntityTypeBuilder)ToTable((EntityTypeBuilder)entityTypeBuilder, name, excludedFromMigrations: false); + + /// + /// Configures the table that the entity type maps to when targeting a relational database. + /// + /// The entity type being configured. + /// The builder for the entity type being configured. + /// The name of the table. + /// A value indicating whether the table should be managed by migrations. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder ToTable( + [NotNull] this EntityTypeBuilder entityTypeBuilder, + [CanBeNull] string name, + bool excludedFromMigrations) + where TEntity : class + => (EntityTypeBuilder)ToTable((EntityTypeBuilder)entityTypeBuilder, name, excludedFromMigrations); /// /// Configures the table that the entity type maps to when targeting a relational database. @@ -59,6 +87,21 @@ public static EntityTypeBuilder ToTable( [NotNull] this EntityTypeBuilder entityTypeBuilder, [CanBeNull] string name, [CanBeNull] string schema) + => entityTypeBuilder.ToTable(name, schema, excludedFromMigrations: false); + + /// + /// Configures the table that the entity type maps to when targeting a relational database. + /// + /// The builder for the entity type being configured. + /// The name of the table. + /// The schema of the table. + /// A value indicating whether the table should be managed by migrations. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder ToTable( + [NotNull] this EntityTypeBuilder entityTypeBuilder, + [CanBeNull] string name, + [CanBeNull] string schema, + bool excludedFromMigrations) { Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); Check.NullButNotEmpty(name, nameof(name)); @@ -66,7 +109,7 @@ public static EntityTypeBuilder ToTable( entityTypeBuilder.Metadata.SetTableName(name); entityTypeBuilder.Metadata.SetSchema(schema); - entityTypeBuilder.Metadata.RemoveAnnotation(RelationalAnnotationNames.ViewDefinitionSql); + entityTypeBuilder.Metadata.SetIsTableExcludedFromMigrations(excludedFromMigrations); return entityTypeBuilder; } @@ -84,7 +127,24 @@ public static EntityTypeBuilder ToTable( [CanBeNull] string name, [CanBeNull] string schema) where TEntity : class - => (EntityTypeBuilder)ToTable((EntityTypeBuilder)entityTypeBuilder, name, schema); + => (EntityTypeBuilder)ToTable((EntityTypeBuilder)entityTypeBuilder, name, schema, excludedFromMigrations: false); + + /// + /// Configures the table that the entity type maps to when targeting a relational database. + /// + /// The entity type being configured. + /// The builder for the entity type being configured. + /// The name of the table. + /// The schema of the table. + /// A value indicating whether the table should be managed by migrations. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder ToTable( + [NotNull] this EntityTypeBuilder entityTypeBuilder, + [CanBeNull] string name, + [CanBeNull] string schema, + bool excludedFromMigrations) + where TEntity : class + => (EntityTypeBuilder)ToTable((EntityTypeBuilder)entityTypeBuilder, name, schema, excludedFromMigrations); /// /// Configures the table that the entity type maps to when targeting a relational database. @@ -95,11 +155,26 @@ public static EntityTypeBuilder ToTable( public static OwnedNavigationBuilder ToTable( [NotNull] this OwnedNavigationBuilder referenceOwnershipBuilder, [CanBeNull] string name) + => referenceOwnershipBuilder.ToTable(name, excludedFromMigrations: false); + + /// + /// Configures the table that the entity type maps to when targeting a relational database. + /// + /// The builder for the entity type being configured. + /// The name of the table. + /// A value indicating whether the table should be managed by migrations. + /// The same builder instance so that multiple calls can be chained. + public static OwnedNavigationBuilder ToTable( + [NotNull] this OwnedNavigationBuilder referenceOwnershipBuilder, + [CanBeNull] string name, + bool excludedFromMigrations) { Check.NotNull(referenceOwnershipBuilder, nameof(referenceOwnershipBuilder)); Check.NullButNotEmpty(name, nameof(name)); referenceOwnershipBuilder.OwnedEntityType.SetTableName(name); + referenceOwnershipBuilder.OwnedEntityType.SetSchema(null); + referenceOwnershipBuilder.OwnedEntityType.SetIsTableExcludedFromMigrations(excludedFromMigrations); return referenceOwnershipBuilder; } @@ -117,7 +192,26 @@ public static OwnedNavigationBuilder ToTable (OwnedNavigationBuilder)ToTable((OwnedNavigationBuilder)referenceOwnershipBuilder, name); + => (OwnedNavigationBuilder)ToTable( + (OwnedNavigationBuilder)referenceOwnershipBuilder, name, excludedFromMigrations: false); + + /// + /// Configures the table that the entity type maps to when targeting a relational database. + /// + /// The entity type being configured. + /// The entity type that this relationship targets. + /// The builder for the entity type being configured. + /// The name of the table. + /// A value indicating whether the table should be managed by migrations. + /// The same builder instance so that multiple calls can be chained. + public static OwnedNavigationBuilder ToTable( + [NotNull] this OwnedNavigationBuilder referenceOwnershipBuilder, + [CanBeNull] string name, + bool excludedFromMigrations) + where TEntity : class + where TRelatedEntity : class + => (OwnedNavigationBuilder)ToTable( + (OwnedNavigationBuilder)referenceOwnershipBuilder, name, excludedFromMigrations); /// /// Configures the table that the entity type maps to when targeting a relational database. @@ -130,6 +224,21 @@ public static OwnedNavigationBuilder ToTable( [NotNull] this OwnedNavigationBuilder referenceOwnershipBuilder, [CanBeNull] string name, [CanBeNull] string schema) + => referenceOwnershipBuilder.ToTable(name, schema, excludedFromMigrations: false); + + /// + /// Configures the table that the entity type maps to when targeting a relational database. + /// + /// The builder for the entity type being configured. + /// The name of the table. + /// The schema of the table. + /// A value indicating whether the table should be managed by migrations. + /// The same builder instance so that multiple calls can be chained. + public static OwnedNavigationBuilder ToTable( + [NotNull] this OwnedNavigationBuilder referenceOwnershipBuilder, + [CanBeNull] string name, + [CanBeNull] string schema, + bool excludedFromMigrations) { Check.NotNull(referenceOwnershipBuilder, nameof(referenceOwnershipBuilder)); Check.NullButNotEmpty(name, nameof(name)); @@ -137,6 +246,7 @@ public static OwnedNavigationBuilder ToTable( referenceOwnershipBuilder.OwnedEntityType.SetTableName(name); referenceOwnershipBuilder.OwnedEntityType.SetSchema(schema); + referenceOwnershipBuilder.OwnedEntityType.SetIsTableExcludedFromMigrations(excludedFromMigrations); return referenceOwnershipBuilder; } @@ -156,7 +266,28 @@ public static OwnedNavigationBuilder ToTable (OwnedNavigationBuilder)ToTable((OwnedNavigationBuilder)referenceOwnershipBuilder, name, schema); + => (OwnedNavigationBuilder)ToTable( + (OwnedNavigationBuilder)referenceOwnershipBuilder, name, schema, excludedFromMigrations: false); + + /// + /// Configures the table that the entity type maps to when targeting a relational database. + /// + /// The entity type being configured. + /// The entity type that this relationship targets. + /// The builder for the entity type being configured. + /// The name of the table. + /// The schema of the table. + /// A value indicating whether the table should be managed by migrations. + /// The same builder instance so that multiple calls can be chained. + public static OwnedNavigationBuilder ToTable( + [NotNull] this OwnedNavigationBuilder referenceOwnershipBuilder, + [CanBeNull] string name, + [CanBeNull] string schema, + bool excludedFromMigrations) + where TEntity : class + where TRelatedEntity : class + => (OwnedNavigationBuilder)ToTable( + (OwnedNavigationBuilder)referenceOwnershipBuilder, name, schema, excludedFromMigrations); /// /// Configures the table that the entity type maps to when targeting a relational database. @@ -209,7 +340,7 @@ public static IConventionEntityTypeBuilder ToTable( /// /// Returns a value indicating whether the view or table name can be set for this entity type - /// from the current configuration source + /// using the specified configuration source. /// /// The builder for the entity type being configured. /// The name of the view or table. @@ -249,7 +380,7 @@ public static IConventionEntityTypeBuilder ToSchema( /// /// Returns a value indicating whether the schema of the view or table name can be set for this entity type - /// from the current configuration source + /// using the specified configuration source. /// /// The builder for the entity type being configured. /// The schema of the view or table. @@ -265,6 +396,43 @@ public static bool CanSetSchema( return entityTypeBuilder.CanSetAnnotation(RelationalAnnotationNames.Schema, schema, fromDataAnnotation); } + /// + /// Mark the table that this entity type is mapped to as excluded from migrations. + /// + /// The builder for the entity type being configured. + /// A value indicating whether the table should be managed by migrations. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + public static IConventionEntityTypeBuilder ExcludeTableFromMigrations( + [NotNull] this IConventionEntityTypeBuilder entityTypeBuilder, + bool? excludedFromMigrations, + bool fromDataAnnotation = false) + { + if (!entityTypeBuilder.CanExcludeTableFromMigrations(excludedFromMigrations, fromDataAnnotation)) + { + return null; + } + + entityTypeBuilder.Metadata.SetIsTableExcludedFromMigrations(excludedFromMigrations, fromDataAnnotation); + return entityTypeBuilder; + } + + /// + /// Returns a value indicating whether the table that this entity type is mapped to can be excluded from migrations + /// using the specified configuration source. + /// + /// The builder for the entity type being configured. + /// A value indicating whether the table should be managed by migrations. + /// Indicates whether the configuration was specified using a data annotation. + /// if the configuration can be applied. + public static bool CanExcludeTableFromMigrations( + [NotNull] this IConventionEntityTypeBuilder entityTypeBuilder, bool? excludedFromMigrations, bool fromDataAnnotation = false) + => entityTypeBuilder.CanSetAnnotation + (RelationalAnnotationNames.IsTableExcludedFromMigrations, excludedFromMigrations, fromDataAnnotation); + /// /// Configures the view that the entity type maps to when targeting a relational database. /// @@ -522,7 +690,7 @@ public static IConventionEntityTypeBuilder HasComment( /// /// Returns a value indicating whether a comment can be set for this entity type - /// from the current configuration source + /// using the specified configuration source. /// /// The builder for the entity type being configured. /// The comment for the table. diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs index c2ac76b0151..6699845fcf8 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs @@ -594,37 +594,65 @@ public static IEnumerable FindViewRowInternalForeignKeys( => entityType.FindRowInternalForeignKeys(name, schema, StoreObjectType.View).Cast(); /// - /// Gets a value indicating whether the entity type is ignored by Migrations. + /// Gets a value indicating whether the associated table is ignored by Migrations. /// /// The entity type. - /// A value indicating whether the entity type is ignored by Migrations. - public static bool IsIgnoredByMigrations([NotNull] this IEntityType entityType) + /// A value indicating whether the associated table is ignored by Migrations. + public static bool IsTableExcludedFromMigrations([NotNull] this IEntityType entityType) { - if (entityType.BaseType != null - && entityType.BaseType.IsIgnoredByMigrations()) + var excluded = (bool?)entityType[RelationalAnnotationNames.IsTableExcludedFromMigrations]; + if (excluded != null) { - return true; + return excluded.Value; } - if (entityType.GetTableName() != null) + if (entityType.FindAnnotation(RelationalAnnotationNames.TableName) != null) { return false; } - var viewDefinitionSql = entityType.FindAnnotation(RelationalAnnotationNames.ViewDefinitionSql); - if (viewDefinitionSql?.Value != null) + if (entityType.BaseType != null) { - return false; + return entityType.GetRootType().IsTableExcludedFromMigrations(); } var ownership = entityType.FindOwnership(); if (ownership != null && ownership.IsUnique) { - return ownership.PrincipalEntityType.IsIgnoredByMigrations(); + return ownership.PrincipalEntityType.IsTableExcludedFromMigrations(); } - return true; + return false; } + + /// + /// Sets a value indicating whether the associated table is ignored by Migrations. + /// + /// The entity type. + /// A value indicating whether the associated table is ignored by Migrations. + public static void SetIsTableExcludedFromMigrations([NotNull] this IMutableEntityType entityType, bool? excluded) + => entityType.SetOrRemoveAnnotation(RelationalAnnotationNames.IsTableExcludedFromMigrations, excluded); + + /// + /// Sets a value indicating whether the associated table is ignored by Migrations. + /// + /// The entity type. + /// A value indicating whether the associated table is ignored by Migrations. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static bool? SetIsTableExcludedFromMigrations( + [NotNull] this IConventionEntityType entityType, bool? excluded, bool fromDataAnnotation = false) + => (bool?)entityType.SetOrRemoveAnnotation(RelationalAnnotationNames.IsTableExcludedFromMigrations, excluded, fromDataAnnotation) + ?.Value; + + /// + /// Gets the for . + /// + /// The entity type to find configuration source for. + /// The for . + public static ConfigurationSource? GetIsTableExcludedFromMigrationsConfigurationSource([NotNull] this IConventionEntityType entityType) + => entityType.FindAnnotation(RelationalAnnotationNames.IsTableExcludedFromMigrations) + ?.GetConfigurationSource(); } } diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index d9404559451..bc512f58907 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs @@ -268,7 +268,9 @@ protected virtual void ValidateSharedTableCompatibility( while (typesToValidate.Count > 0) { var entityType = typesToValidate.Dequeue(); + var key = entityType.FindPrimaryKey(); var comment = entityType.GetComment(); + var isExcluded = entityType.IsTableExcludedFromMigrations(); var typesToValidateLeft = typesToValidate.Count; var directlyConnectedTypes = unvalidatedTypes.Where( unvalidatedType => @@ -277,7 +279,6 @@ protected virtual void ValidateSharedTableCompatibility( foreach (var nextEntityType in directlyConnectedTypes) { - var key = entityType.FindPrimaryKey(); var otherKey = nextEntityType.FindPrimaryKey(); if (key?.GetName(tableName, schema) != otherKey?.GetName(tableName, schema)) { @@ -312,6 +313,15 @@ protected virtual void ValidateSharedTableCompatibility( comment = nextComment; } + if (isExcluded.Equals(!nextEntityType.IsTableExcludedFromMigrations())) + { + throw new InvalidOperationException( + RelationalStrings.IncompatibleTableExcludedMismatch( + Format(tableName, schema), + entityType.DisplayName(), + nextEntityType.DisplayName())); + } + typesToValidate.Enqueue(nextEntityType); } diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs index 8211b7e127d..664c639fc63 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs @@ -8,6 +8,7 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Metadata.Internal { @@ -101,8 +102,14 @@ public static IModel Add( databaseModel.Tables.Add((mappedTable, mappedSchema), table); } - table.IsExcludedFromMigrations = table.IsExcludedFromMigrations - && entityType.FindAnnotation(RelationalAnnotationNames.ViewDefinitionSql) != null; + if (mappedType == entityType) + { + Check.DebugAssert(table.EntityTypeMappings.Count == 0 + || table.IsExcludedFromMigrations == entityType.IsTableExcludedFromMigrations(), + "Table should be excluded on all entity types"); + + table.IsExcludedFromMigrations = entityType.IsTableExcludedFromMigrations(); + } var tableMapping = new TableMapping(entityType, table, includesDerivedTypes: true); foreach (var property in mappedType.GetProperties()) diff --git a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs index 57c6c1d229e..6ae77116b25 100644 --- a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs +++ b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs @@ -138,6 +138,11 @@ public static class RelationalAnnotationNames /// public const string ViewDefinitionSql = Prefix + "ViewDefinitionSql"; + /// + /// The name for the annotation determining whether the table is excluded from migrations. + /// + public const string IsTableExcludedFromMigrations = Prefix + "IsTableExcludedFromMigrations"; + /// /// 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 2ba4399f0df..62dce1d6429 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -873,6 +873,14 @@ public static string IncompatibleTableDerivedRelationship([CanBeNull] object tab GetString("IncompatibleTableDerivedRelationship", nameof(table), nameof(entityType), nameof(otherEntityType)), table, entityType, otherEntityType); + /// + /// Cannot use table '{table}' for entity type '{entityType}' since it is being used for entity type '{otherEntityType}' and it excluded from migration on one entity type, but not the other. Exclude the table from migrations on all entity types mapped to the table. + /// + public static string IncompatibleTableExcludedMismatch([CanBeNull] object table, [CanBeNull] object entityType, [CanBeNull] object otherEntityType) + => string.Format( + GetString("IncompatibleTableExcludedMismatch", nameof(table), nameof(entityType), nameof(otherEntityType)), + table, entityType, otherEntityType); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 67123852f28..641d0f75e01 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -657,4 +657,7 @@ Cannot use table '{table}' for entity type '{entityType}' since it is being used for entity type '{otherEntityType}', there is a relationship between their primary keys in which '{entityType}' is the dependent and '{entityType}' has a base entity type mapped to a different table. Either map '{otherEntityType}' to a different table or invert the relationship between '{entityType}' and '{otherEntityType}'. + + Cannot use table '{table}' for entity type '{entityType}' since it is being used for entity type '{otherEntityType}' and it excluded from migration on one entity type, but not the other. Exclude the table from migrations on all entity types mapped to the table. + \ No newline at end of file diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs index 310f85e1cb0..dfe0caf56a1 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs @@ -93,7 +93,7 @@ public void Test_new_annotations_handled_for_entity_types() _nl + "modelBuilder." + nameof(RelationalEntityTypeBuilderExtensions.ToTable) - + @"(""WithAnnotations"",""MySchema"");" + + @"(""WithAnnotations"", ""MySchema"");" + _nl) }, { diff --git a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs index a1b199d620a..4da163dc8d8 100644 --- a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs +++ b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs @@ -374,7 +374,7 @@ public virtual void Entities_are_stored_in_model_snapshot_for_TPT() builder => { builder.Entity() - .ToTable("DerivedEntity"); + .ToTable("DerivedEntity", "foo"); builder.Entity(); }, AddBoilerPlate( @@ -402,7 +402,7 @@ public virtual void Entities_are_stored_in_model_snapshot_for_TPT() b.Property(""Name"") .HasColumnType(""nvarchar(max)""); - b.ToTable(""DerivedEntity""); + b.ToTable(""DerivedEntity"", ""foo""); });"), o => { @@ -414,12 +414,71 @@ public virtual void Entities_are_stored_in_model_snapshot_for_TPT() } [ConditionalFact] - public void Views_are_ignored() + public virtual void Entities_are_stored_in_model_snapshot_for_TPT_with_one_excluded() + { + Test( + builder => + { + builder.Entity() + .ToTable("DerivedEntity", "foo", excludedFromMigrations: true); + builder.Entity(); + }, + AddBoilerPlate( + GetHeading() + + @" + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+BaseEntity"", b => + { + b.Property(""Id"") + .ValueGeneratedOnAdd() + .HasColumnType(""int"") + .UseIdentityColumn(); + + b.Property(""Discriminator"") + .HasColumnType(""nvarchar(max)""); + + b.HasKey(""Id""); + + b.ToTable(""BaseEntity""); + }); + + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+DerivedEntity"", b => + { + b.HasBaseType(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+BaseEntity""); + + b.Property(""Name"") + .HasColumnType(""nvarchar(max)""); + + b.ToTable(""DerivedEntity"", ""foo"", true); + });"), + o => + { + Assert.Equal(5, o.GetAnnotations().Count()); + + Assert.Equal("DerivedEntity", + o.FindEntityType("Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+DerivedEntity").GetTableName()); + }); + } + + [ConditionalFact] + public void Views_are_stored_in_the_model_snapshot() { Test( builder => builder.Entity().Ignore(e => e.EntityWithTwoProperties).ToView("EntityWithOneProperty"), - AddBoilerPlate(GetHeading(empty: true)), - o => Assert.Empty(o.GetEntityTypes())); + + AddBoilerPlate( + GetHeading() + + @" + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithOneProperty"", b => + { + b.Property(""Id"") + .HasColumnType(""int"") + .UseIdentityColumn(); + + b.HasKey(""Id""); + + b.ToView(""EntityWithOneProperty""); + });"), + o => Assert.Equal("EntityWithOneProperty", o.GetEntityTypes().Single().GetViewName())); } [ConditionalFact] @@ -577,7 +636,7 @@ public virtual void EntityType_annotations_are_stored_in_snapshot() });"), o => { - Assert.Equal(4, o.GetEntityTypes().First().GetAnnotations().Count()); + Assert.Equal(5, o.GetEntityTypes().First().GetAnnotations().Count()); Assert.Equal("AnnotationValue", o.GetEntityTypes().First()["AnnotationName"]); }); } diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index 70b495924d4..da064840fc2 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs @@ -353,17 +353,62 @@ public virtual void Passes_for_compatible_shared_table() } [ConditionalFact] - public virtual void Passes_for_compatible_shared_table_inverted() + public virtual void Passes_for_compatible_excluded_shared_table_inverted() { var modelBuilder = CreateConventionalModelBuilder(); modelBuilder.Entity().HasOne().WithOne().IsRequired().HasPrincipalKey(a => a.Id).HasForeignKey(b => b.Id); - modelBuilder.Entity().ToTable("Table"); - modelBuilder.Entity().ToTable("Table"); + modelBuilder.Entity().ToTable("Table", excludedFromMigrations: true); + modelBuilder.Entity().ToTable("Table", excludedFromMigrations: true); Validate(modelBuilder.Model); } + [ConditionalFact] + public virtual void Passes_for_compatible_excluded_shared_table_owned() + { + var modelBuilder = CreateConventionalModelBuilder(); + + modelBuilder.Entity().OwnsOne(b => b.A); + modelBuilder.Entity().ToTable("Table", excludedFromMigrations: true); + + var model = Validate(modelBuilder.Model); + + var b = model.FindEntityType(typeof(B)); + Assert.Equal("Table", b.GetTableName()); + Assert.True(b.IsTableExcludedFromMigrations()); + } + + [ConditionalFact] + public virtual void Passes_for_compatible_excluded_table_derived() + { + var modelBuilder = CreateConventionalModelBuilder(); + + modelBuilder.Entity().ToTable("Table", excludedFromMigrations: true); + modelBuilder.Entity(); + + var model = Validate(modelBuilder.Model); + + var c = model.FindEntityType(typeof(C)); + Assert.Equal("Table", c.GetTableName()); + Assert.True(c.IsTableExcludedFromMigrations()); + } + + [ConditionalFact] + public virtual void Detect_partially_excluded_shared_table() + { + var modelBuilder = CreateConventionalModelBuilder(); + + modelBuilder.Entity().HasOne().WithOne().IsRequired().HasPrincipalKey(a => a.Id).HasForeignKey(b => b.Id); + modelBuilder.Entity().ToTable("Table", excludedFromMigrations: true); + modelBuilder.Entity().ToTable("Table"); + + VerifyError( + RelationalStrings.IncompatibleTableExcludedMismatch( + nameof(Table), nameof(A), nameof(B)), + modelBuilder.Model); + } + [ConditionalFact] public virtual void Detects_duplicate_column_names() { diff --git a/test/EFCore.Relational.Tests/RelationalApiConsistencyTest.cs b/test/EFCore.Relational.Tests/RelationalApiConsistencyTest.cs index f9aaa1062ae..ff42971587c 100644 --- a/test/EFCore.Relational.Tests/RelationalApiConsistencyTest.cs +++ b/test/EFCore.Relational.Tests/RelationalApiConsistencyTest.cs @@ -123,7 +123,8 @@ public override bool TryGetProviderOptionsDelegate(out Action UnmatchedMetadataMethods { get; } = new HashSet { typeof(IDbFunction).GetMethod("get_ReturnEntityType"), - typeof(RelationalPropertyExtensions).GetMethod(nameof(RelationalPropertyExtensions.FindOverrides)) + typeof(RelationalPropertyExtensions).GetMethod(nameof(RelationalPropertyExtensions.FindOverrides)), + typeof(RelationalEntityTypeBuilderExtensions).GetMethod(nameof(RelationalEntityTypeBuilderExtensions.ExcludeTableFromMigrations)) }; public override HashSet AsyncMethodExceptions { get; } = new HashSet diff --git a/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderGenericTest.cs b/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderGenericTest.cs index a6f6e6135d6..3aadfc33e3d 100644 --- a/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderGenericTest.cs +++ b/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderGenericTest.cs @@ -491,7 +491,7 @@ public virtual void Owned_type_collections_can_be_mapped_to_different_tables() modelBuilder.FinalizeModel(); Assert.Equal("blah", owned.GetTableName()); - Assert.Equal("foo", owned.GetSchema()); + Assert.Null(owned.GetSchema()); } [ConditionalFact]