From 3414478857998d94a3387db6ef042ae0ba7d2f9b Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 28 Feb 2026 17:11:55 +0200 Subject: [PATCH 1/4] Allow excluding foreign key from migrations Closes #15854 --- .../Design/AnnotationCodeGenerator.cs | 7 + ...nalCSharpRuntimeAnnotationCodeGenerator.cs | 1 + .../RelationalForeignKeyBuilderExtensions.cs | 162 ++++++++++++++++++ .../RelationalForeignKeyExtensions.cs | 41 +++++ .../RelationalRuntimeModelConvention.cs | 1 + .../Metadata/IForeignKeyConstraint.cs | 5 + .../Metadata/Internal/ForeignKeyConstraint.cs | 9 + .../RelationalForeignKeyExtensions.cs | 17 ++ .../Metadata/RelationalAnnotationNames.cs | 6 + .../Internal/MigrationsModelDiffer.cs | 6 +- .../Properties/RelationalStrings.Designer.cs | 8 + .../Properties/RelationalStrings.resx | 3 + ...rpMigrationsGeneratorTest.ModelSnapshot.cs | 66 +++++++ .../Design/CSharpMigrationsGeneratorTest.cs | 6 +- .../Migrations/MigrationsTestBase.cs | 27 +++ .../Design/AnnotationCodeGeneratorTest.cs | 37 ++++ .../RelationalBuilderExtensionsTest.cs | 101 +++++++++++ .../RelationalMetadataExtensionsTest.cs | 31 ++++ .../RelationalModelValidatorTest.cs | 18 ++ .../Internal/MigrationsModelDifferTest.cs | 49 ++++++ .../Migrations/MigrationsSqlServerTest.cs | 10 ++ .../Migrations/MigrationsSqliteTest.cs | 10 ++ 22 files changed, 617 insertions(+), 4 deletions(-) diff --git a/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs index 1762c7e9f1c..8f1abadd947 100644 --- a/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs +++ b/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs @@ -435,6 +435,12 @@ public virtual IReadOnlyList GenerateFluentApiCalls( nameof(RelationalForeignKeyBuilderExtensions.HasConstraintName), methodCallCodeFragments); + GenerateSimpleFluentApiCall( + annotations, + RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations, + nameof(RelationalForeignKeyBuilderExtensions.ExcludeFromMigrations), + methodCallCodeFragments); + methodCallCodeFragments.AddRange(GenerateFluentApiCallsHelper(foreignKey, annotations, GenerateFluentApi)); return methodCallCodeFragments; @@ -1051,6 +1057,7 @@ private static void GenerateSimpleFluentApiCall( if (annotations.TryGetValue(annotationName, out var annotation)) { annotations.Remove(annotationName); + if (annotation.Value is { } annotationValue) { methodCallCodeFragments.Add( diff --git a/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs index ed97fd13171..0f3435229c4 100644 --- a/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs +++ b/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs @@ -2167,6 +2167,7 @@ public override void Generate(IForeignKey foreignKey, CSharpRuntimeAnnotationCod if (parameters.IsRuntime) { parameters.Annotations.Remove(RelationalAnnotationNames.ForeignKeyMappings); + parameters.Annotations.Remove(RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations); } base.Generate(foreignKey, parameters); diff --git a/src/EFCore.Relational/Extensions/RelationalForeignKeyBuilderExtensions.cs b/src/EFCore.Relational/Extensions/RelationalForeignKeyBuilderExtensions.cs index 718cc713542..6b05406bd48 100644 --- a/src/EFCore.Relational/Extensions/RelationalForeignKeyBuilderExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalForeignKeyBuilderExtensions.cs @@ -173,4 +173,166 @@ public static bool CanSetConstraintName( string? name, bool fromDataAnnotation = false) => relationship.CanSetAnnotation(RelationalAnnotationNames.Name, name, fromDataAnnotation); + + /// + /// Configures whether the foreign key constraint is excluded from migrations + /// when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The builder being used to configure the relationship. + /// A value indicating whether the foreign key constraint is excluded from migrations. + /// The same builder instance so that multiple calls can be chained. + public static ReferenceCollectionBuilder ExcludeFromMigrations( + this ReferenceCollectionBuilder referenceCollectionBuilder, + bool excluded = true) + { + referenceCollectionBuilder.Metadata.SetIsExcludedFromMigrations(excluded); + + return referenceCollectionBuilder; + } + + /// + /// Configures whether the foreign key constraint is excluded from migrations + /// when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The builder being used to configure the relationship. + /// A value indicating whether the foreign key constraint is excluded from migrations. + /// The same builder instance so that multiple calls can be chained. + /// The principal entity type in this relationship. + /// The dependent entity type in this relationship. + public static ReferenceCollectionBuilder ExcludeFromMigrations( + this ReferenceCollectionBuilder referenceCollectionBuilder, + bool excluded = true) + where TEntity : class + where TRelatedEntity : class + => (ReferenceCollectionBuilder)ExcludeFromMigrations( + (ReferenceCollectionBuilder)referenceCollectionBuilder, excluded); + + /// + /// Configures whether the foreign key constraint is excluded from migrations + /// when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The builder being used to configure the relationship. + /// A value indicating whether the foreign key constraint is excluded from migrations. + /// The same builder instance so that multiple calls can be chained. + public static ReferenceReferenceBuilder ExcludeFromMigrations( + this ReferenceReferenceBuilder referenceReferenceBuilder, + bool excluded = true) + { + referenceReferenceBuilder.Metadata.SetIsExcludedFromMigrations(excluded); + + return referenceReferenceBuilder; + } + + /// + /// Configures whether the foreign key constraint is excluded from migrations + /// when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The builder being used to configure the relationship. + /// A value indicating whether the foreign key constraint is excluded from migrations. + /// The same builder instance so that multiple calls can be chained. + /// The entity type on one end of the relationship. + /// The entity type on the other end of the relationship. + public static ReferenceReferenceBuilder ExcludeFromMigrations( + this ReferenceReferenceBuilder referenceReferenceBuilder, + bool excluded = true) + where TEntity : class + where TRelatedEntity : class + => (ReferenceReferenceBuilder)ExcludeFromMigrations( + (ReferenceReferenceBuilder)referenceReferenceBuilder, excluded); + + /// + /// Configures whether the foreign key constraint is excluded from migrations + /// when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The builder being used to configure the relationship. + /// A value indicating whether the foreign key constraint is excluded from migrations. + /// The same builder instance so that multiple calls can be chained. + public static OwnershipBuilder ExcludeFromMigrations( + this OwnershipBuilder ownershipBuilder, + bool excluded = true) + { + ownershipBuilder.Metadata.SetIsExcludedFromMigrations(excluded); + + return ownershipBuilder; + } + + /// + /// Configures whether the foreign key constraint is excluded from migrations + /// when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The builder being used to configure the relationship. + /// A value indicating whether the foreign key constraint is excluded from migrations. + /// The same builder instance so that multiple calls can be chained. + /// The entity type on one end of the relationship. + /// The entity type on the other end of the relationship. + public static OwnershipBuilder ExcludeFromMigrations( + this OwnershipBuilder ownershipBuilder, + bool excluded = true) + where TEntity : class + where TDependentEntity : class + => (OwnershipBuilder)ExcludeFromMigrations( + (OwnershipBuilder)ownershipBuilder, excluded); + + /// + /// Configures whether the foreign key constraint is excluded from migrations + /// when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The builder being used to configure the relationship. + /// A value indicating whether the foreign key constraint is excluded from migrations. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + public static IConventionForeignKeyBuilder? ExcludeFromMigrations( + this IConventionForeignKeyBuilder relationship, + bool? excluded, + bool fromDataAnnotation = false) + { + if (!relationship.CanSetExcludeFromMigrations(excluded, fromDataAnnotation)) + { + return null; + } + + relationship.Metadata.SetIsExcludedFromMigrations(excluded, fromDataAnnotation); + return relationship; + } + + /// + /// Returns a value indicating whether the foreign key constraint exclusion from migrations can be set + /// from the current configuration source. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The builder being used to configure the relationship. + /// A value indicating whether the foreign key constraint is excluded from migrations. + /// Indicates whether the configuration was specified using a data annotation. + /// if the configuration can be applied. + public static bool CanSetExcludeFromMigrations( + this IConventionForeignKeyBuilder relationship, + bool? excluded, + bool fromDataAnnotation = false) + => relationship.CanSetAnnotation(RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations, excluded, fromDataAnnotation); } diff --git a/src/EFCore.Relational/Extensions/RelationalForeignKeyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalForeignKeyExtensions.cs index 985ac838e1f..640804bfa23 100644 --- a/src/EFCore.Relational/Extensions/RelationalForeignKeyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalForeignKeyExtensions.cs @@ -288,4 +288,45 @@ static bool IsMapped(IReadOnlyForeignKey foreignKey, StoreObjectIdentifier store this IForeignKey foreignKey, in StoreObjectIdentifier storeObject) => (IForeignKey?)((IReadOnlyForeignKey)foreignKey).FindSharedObjectRootForeignKey(storeObject); + + /// + /// Returns a value indicating whether the foreign key constraint is excluded from migrations. + /// + /// The foreign key. + /// if the foreign key constraint is excluded from migrations. + public static bool IsExcludedFromMigrations(this IReadOnlyForeignKey foreignKey) + => (bool?)foreignKey[RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations] ?? false; + + /// + /// Sets a value indicating whether the foreign key constraint is excluded from migrations. + /// + /// The foreign key. + /// The value to set. + public static void SetIsExcludedFromMigrations(this IMutableForeignKey foreignKey, bool? excluded) + => foreignKey.SetOrRemoveAnnotation(RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations, excluded); + + /// + /// Sets a value indicating whether the foreign key constraint is excluded from migrations. + /// + /// The foreign key. + /// The value to set. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static bool? SetIsExcludedFromMigrations( + this IConventionForeignKey foreignKey, + bool? excluded, + bool fromDataAnnotation = false) + => (bool?)foreignKey.SetOrRemoveAnnotation( + RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations, + excluded, + fromDataAnnotation)?.Value; + + /// + /// Gets the for the foreign key exclusion from migrations. + /// + /// The foreign key. + /// The for the foreign key exclusion from migrations. + public static ConfigurationSource? GetIsExcludedFromMigrationsConfigurationSource(this IConventionForeignKey foreignKey) + => foreignKey.FindAnnotation(RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations) + ?.GetConfigurationSource(); } diff --git a/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs b/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs index 20aeb33cd38..5a31e791746 100644 --- a/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs @@ -505,6 +505,7 @@ protected override void ProcessForeignKeyAnnotations( if (runtime) { annotations.Remove(RelationalAnnotationNames.ForeignKeyMappings); + annotations.Remove(RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations); } } diff --git a/src/EFCore.Relational/Metadata/IForeignKeyConstraint.cs b/src/EFCore.Relational/Metadata/IForeignKeyConstraint.cs index a89311c9b13..27e5b87e837 100644 --- a/src/EFCore.Relational/Metadata/IForeignKeyConstraint.cs +++ b/src/EFCore.Relational/Metadata/IForeignKeyConstraint.cs @@ -55,6 +55,11 @@ IReadOnlyList PrincipalColumns /// ReferentialAction OnDeleteAction { get; } + /// + /// Gets a value indicating whether the foreign key constraint is excluded from migrations. + /// + bool IsExcludedFromMigrations { get; } + /// /// /// Creates a human-readable representation of the given metadata. diff --git a/src/EFCore.Relational/Metadata/Internal/ForeignKeyConstraint.cs b/src/EFCore.Relational/Metadata/Internal/ForeignKeyConstraint.cs index 53d904470e6..e23cb6144b0 100644 --- a/src/EFCore.Relational/Metadata/Internal/ForeignKeyConstraint.cs +++ b/src/EFCore.Relational/Metadata/Internal/ForeignKeyConstraint.cs @@ -102,6 +102,15 @@ public override bool IsReadOnly /// public virtual ReferentialAction OnDeleteAction { get; set; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool IsExcludedFromMigrations + => ((IReadOnlyForeignKey)MappedForeignKeys.First()).IsExcludedFromMigrations(); + /// /// 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/Metadata/Internal/RelationalForeignKeyExtensions.cs b/src/EFCore.Relational/Metadata/Internal/RelationalForeignKeyExtensions.cs index 4c7460bb7dc..9e49913facb 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalForeignKeyExtensions.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalForeignKeyExtensions.cs @@ -161,6 +161,23 @@ is not { } principalColumns return false; } + if (foreignKey.IsExcludedFromMigrations() != duplicateForeignKey.IsExcludedFromMigrations()) + { + if (shouldThrow) + { + throw new InvalidOperationException( + RelationalStrings.DuplicateForeignKeyExcludedFromMigrationsMismatch( + foreignKey.Properties.Format(), + foreignKey.DeclaringEntityType.DisplayName(), + duplicateForeignKey.Properties.Format(), + duplicateForeignKey.DeclaringEntityType.DisplayName(), + foreignKey.DeclaringEntityType.GetSchemaQualifiedTableName(), + foreignKey.GetConstraintName(storeObject, principalTable.Value))); + } + + return false; + } + return true; } diff --git a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs index 97084eee01a..029db49c5a6 100644 --- a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs +++ b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs @@ -174,6 +174,11 @@ public static class RelationalAnnotationNames /// public const string IsTableExcludedFromMigrations = Prefix + "IsTableExcludedFromMigrations"; + /// + /// The name for the annotation determining whether the foreign key constraint is excluded from migrations. + /// + public const string IsForeignKeyExcludedFromMigrations = Prefix + "IsForeignKeyExcludedFromMigrations"; + /// /// The name for the annotation determining the mapping strategy for inherited properties. /// @@ -396,6 +401,7 @@ public static class RelationalAnnotationNames IsFixedLength, ViewDefinitionSql, IsTableExcludedFromMigrations, + IsForeignKeyExcludedFromMigrations, MappingStrategy, RelationalModel, RelationalModelFactory, diff --git a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs index 555ff93715b..d72ec524220 100644 --- a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs +++ b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs @@ -1429,7 +1429,8 @@ protected virtual IEnumerable Diff( protected virtual IEnumerable Add(IForeignKeyConstraint target, DiffContext diffContext) { var targetTable = target.Table; - if (targetTable.IsExcludedFromMigrations) + if (targetTable.IsExcludedFromMigrations + || target.IsExcludedFromMigrations) { yield break; } @@ -1456,7 +1457,8 @@ protected virtual IEnumerable Add(IForeignKeyConstraint targ protected virtual IEnumerable Remove(IForeignKeyConstraint source, DiffContext diffContext) { var sourceTable = source.Table; - if (sourceTable.IsExcludedFromMigrations) + if (sourceTable.IsExcludedFromMigrations + || source.IsExcludedFromMigrations) { yield break; } diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 879e4b3c93f..681131294a1 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -571,6 +571,14 @@ public static string DuplicateForeignKeyDeleteBehaviorMismatch(object? foreignKe GetString("DuplicateForeignKeyDeleteBehaviorMismatch", nameof(foreignKeyProperties1), nameof(entityType1), nameof(foreignKeyProperties2), nameof(entityType2), nameof(table), nameof(foreignKeyName), nameof(deleteBehavior1), nameof(deleteBehavior2)), foreignKeyProperties1, entityType1, foreignKeyProperties2, entityType2, table, foreignKeyName, deleteBehavior1, deleteBehavior2); + /// + /// The foreign keys {foreignKeyProperties1} on '{entityType1}' and {foreignKeyProperties2} on '{entityType2}' are both mapped to '{table}.{foreignKeyName}', but with different migration exclusion configurations. + /// + public static string DuplicateForeignKeyExcludedFromMigrationsMismatch(object? foreignKeyProperties1, object? entityType1, object? foreignKeyProperties2, object? entityType2, object? table, object? foreignKeyName) + => string.Format( + GetString("DuplicateForeignKeyExcludedFromMigrationsMismatch", nameof(foreignKeyProperties1), nameof(entityType1), nameof(foreignKeyProperties2), nameof(entityType2), nameof(table), nameof(foreignKeyName)), + foreignKeyProperties1, entityType1, foreignKeyProperties2, entityType2, table, foreignKeyName); + /// /// The foreign keys {foreignKeyProperties1} on '{entityType1}' and {foreignKeyProperties2} on '{entityType2}' are both mapped to '{table}.{foreignKeyName}', but referencing different principal columns ({principalColumnNames1} and {principalColumnNames2}). /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 1dbddd15833..85b1642ceeb 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -328,6 +328,9 @@ The foreign keys {foreignKeyProperties1} on '{entityType1}' and {foreignKeyProperties2} on '{entityType2}' are both mapped to '{table}.{foreignKeyName}', but configured with different delete behavior ('{deleteBehavior1}' and '{deleteBehavior2}'). + + The foreign keys {foreignKeyProperties1} on '{entityType1}' and {foreignKeyProperties2} on '{entityType2}' are both mapped to '{table}.{foreignKeyName}', but with different migration exclusion configurations. + The foreign keys {foreignKeyProperties1} on '{entityType1}' and {foreignKeyProperties2} on '{entityType2}' are both mapped to '{table}.{foreignKeyName}', but referencing different principal columns ({principalColumnNames1} and {principalColumnNames2}). diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs index 13e0cf78c93..469d99cf3e7 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs @@ -8017,6 +8017,72 @@ public virtual void ForeignKey_constraint_name_is_stored_in_snapshot_as_fluent_a o => Assert.Equal( "Constraint", o.FindEntityType(typeof(EntityWithTwoProperties)).GetForeignKeys().First()["Relational:Name"])); + [ConditionalFact] + public virtual void ForeignKey_excluded_from_migrations_is_stored_in_snapshot() + => Test( + builder => + { + builder.Entity() + .HasOne(e => e.EntityWithOneProperty) + .WithOne(e => e.EntityWithTwoProperties) + .HasForeignKey(e => e.AlternateId) + .ExcludeFromMigrations(); + }, + AddBoilerPlate( + GetHeading() + + """ + modelBuilder.Entity("Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorTest+EntityWithOneProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.HasKey("Id"); + + b.ToTable("EntityWithOneProperty", "DefaultSchema"); + }); + + modelBuilder.Entity("Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorTest+EntityWithTwoProperties", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlternateId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AlternateId") + .IsUnique(); + + b.ToTable("EntityWithTwoProperties", "DefaultSchema"); + }); + + modelBuilder.Entity("Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorTest+EntityWithTwoProperties", b => + { + b.HasOne("Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorTest+EntityWithOneProperty", "EntityWithOneProperty") + .WithOne("EntityWithTwoProperties") + .HasForeignKey("Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorTest+EntityWithTwoProperties", "AlternateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .ExcludeFromMigrations(true); + + b.Navigation("EntityWithOneProperty"); + }); + + modelBuilder.Entity("Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorTest+EntityWithOneProperty", b => + { + b.Navigation("EntityWithTwoProperties"); + }); +"""), + o => Assert.True( + o.FindEntityType(typeof(EntityWithTwoProperties)).GetForeignKeys().First().IsExcludedFromMigrations())); + [ConditionalFact] public virtual void ForeignKey_multiple_annotations_are_stored_in_snapshot() => Test( diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs index 2625501a591..6c6d3c3e73c 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs @@ -102,7 +102,8 @@ public void Test_new_annotations_handled_for_entity_types() RelationalAnnotationNames.ContainerColumnTypeMapping, #pragma warning restore CS0618 RelationalAnnotationNames.StoreType, - RelationalAnnotationNames.UseNamedDefaultConstraints + RelationalAnnotationNames.UseNamedDefaultConstraints, + RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations }; // Add a line here if the code generator is supposed to handle this annotation @@ -263,7 +264,8 @@ public void Test_new_annotations_handled_for_properties() #pragma warning restore CS0618 RelationalAnnotationNames.JsonPropertyName, RelationalAnnotationNames.StoreType, - RelationalAnnotationNames.UseNamedDefaultConstraints + RelationalAnnotationNames.UseNamedDefaultConstraints, + RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations }; var columnMapping = $@"{_nl}.{nameof(RelationalPropertyBuilderExtensions.HasColumnType)}(""default_int_mapping"")"; diff --git a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs index b5e8c8afcda..43a6e35d9aa 100644 --- a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs @@ -2328,6 +2328,33 @@ public virtual Task Add_foreign_key_with_name() } }); + [ConditionalFact] + public virtual Task Add_foreign_key_excluded_from_migrations() + => Test( + builder => + { + builder.Entity( + "Customers", e => + { + e.Property("Id"); + e.HasKey("Id"); + }); + builder.Entity( + "Orders", e => + { + e.Property("Id"); + e.Property("CustomerId"); + }); + }, + builder => { }, + builder => builder.Entity("Orders").HasOne("Customers").WithMany() + .HasForeignKey("CustomerId").ExcludeFromMigrations(), + model => + { + var ordersTable = Assert.Single(model.Tables, t => t.Name == "Orders"); + Assert.Empty(ordersTable.ForeignKeys); + }); + [ConditionalFact] public virtual Task Drop_foreign_key() => Test( diff --git a/test/EFCore.Relational.Tests/Design/AnnotationCodeGeneratorTest.cs b/test/EFCore.Relational.Tests/Design/AnnotationCodeGeneratorTest.cs index d390013a875..7ac8d15fc2b 100644 --- a/test/EFCore.Relational.Tests/Design/AnnotationCodeGeneratorTest.cs +++ b/test/EFCore.Relational.Tests/Design/AnnotationCodeGeneratorTest.cs @@ -44,6 +44,43 @@ public void GenerateFluentApi_IProperty_works_with_collation() Assert.Equal("foo", Assert.Single(result.Arguments)); } + [ConditionalFact] + public void IsForeignKeyExcludedFromMigrations_false_is_handled_by_convention() + { + var modelBuilder = CreateModelBuilder(); + modelBuilder.Entity("Blog", x => + { + x.Property("Id"); + x.Property("ParentId"); + x.HasOne("Blog").WithMany().HasForeignKey("ParentId"); + }); + var foreignKey = modelBuilder.Model.FindEntityType("Blog").GetForeignKeys().Single(); + + var annotations = foreignKey.GetAnnotations().ToDictionary(a => a.Name, a => a); + CreateGenerator().RemoveAnnotationsHandledByConventions((IForeignKey)foreignKey, annotations); + + Assert.DoesNotContain(RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations, annotations.Keys); + } + + [ConditionalFact] + public void GenerateFluentApi_IForeignKey_works_with_ExcludeFromMigrations() + { + var modelBuilder = CreateModelBuilder(); + modelBuilder.Entity("Blog", x => + { + x.Property("Id"); + x.Property("ParentId"); + x.HasOne("Blog").WithMany().HasForeignKey("ParentId").ExcludeFromMigrations(); + }); + var foreignKey = modelBuilder.Model.FindEntityType("Blog").GetForeignKeys().Single(); + + var annotations = foreignKey.GetAnnotations().ToDictionary(a => a.Name, a => a); + var result = CreateGenerator().GenerateFluentApiCalls((IForeignKey)foreignKey, annotations).Single(); + + Assert.Equal("ExcludeFromMigrations", result.Method); + Assert.Equal(true, Assert.Single(result.Arguments)); + } + private ModelBuilder CreateModelBuilder() => FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); diff --git a/test/EFCore.Relational.Tests/Extensions/RelationalBuilderExtensionsTest.cs b/test/EFCore.Relational.Tests/Extensions/RelationalBuilderExtensionsTest.cs index df43bd34476..28497b96471 100644 --- a/test/EFCore.Relational.Tests/Extensions/RelationalBuilderExtensionsTest.cs +++ b/test/EFCore.Relational.Tests/Extensions/RelationalBuilderExtensionsTest.cs @@ -372,6 +372,69 @@ public void Can_set_foreign_key_name_for_one_to_one_with_FK_specified() Assert.Equal("LemonSupreme", foreignKey.GetConstraintName()); } + [ConditionalFact] + public void Can_set_foreign_key_exclude_from_migrations_for_one_to_many() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder + .Entity().HasMany(e => e.Orders).WithOne(e => e.Customer) + .ExcludeFromMigrations(); + + var foreignKey = modelBuilder.Model.FindEntityType(typeof(Order)).GetForeignKeys() + .Single(fk => fk.PrincipalEntityType.ClrType == typeof(Customer)); + + Assert.True(foreignKey.IsExcludedFromMigrations()); + + modelBuilder + .Entity().HasMany(e => e.Orders).WithOne(e => e.Customer) + .ExcludeFromMigrations(false); + + Assert.False(foreignKey.IsExcludedFromMigrations()); + } + + [ConditionalFact] + public void Can_set_foreign_key_exclude_from_migrations_for_many_to_one() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder + .Entity().HasOne(e => e.Customer).WithMany(e => e.Orders) + .ExcludeFromMigrations(); + + var foreignKey = modelBuilder.Model.FindEntityType(typeof(Order)).GetForeignKeys() + .Single(fk => fk.PrincipalEntityType.ClrType == typeof(Customer)); + + Assert.True(foreignKey.IsExcludedFromMigrations()); + + modelBuilder + .Entity().HasOne(e => e.Customer).WithMany(e => e.Orders) + .ExcludeFromMigrations(false); + + Assert.False(foreignKey.IsExcludedFromMigrations()); + } + + [ConditionalFact] + public void Can_set_foreign_key_exclude_from_migrations_for_one_to_one() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder + .Entity().HasOne(e => e.Details).WithOne(e => e.Order) + .HasPrincipalKey(e => e.OrderId) + .ExcludeFromMigrations(); + + var foreignKey = modelBuilder.Model.FindEntityType(typeof(OrderDetails)).GetForeignKeys().Single(); + + Assert.True(foreignKey.IsExcludedFromMigrations()); + + modelBuilder + .Entity().HasOne(e => e.Details).WithOne(e => e.Order) + .ExcludeFromMigrations(false); + + Assert.False(foreignKey.IsExcludedFromMigrations()); + } + [ConditionalFact] public void Can_access_index() { @@ -1391,6 +1454,26 @@ public void Relational_relationship_methods_dont_break_out_of_the_generics() .HasOne(e => e.Details) .WithOne(e => e.Order) .HasConstraintName("Simon")); + + AssertIsGeneric( + modelBuilder + .Entity().HasMany(e => e.Orders) + .WithOne(e => e.Customer) + .ExcludeFromMigrations()); + + AssertIsGeneric( + modelBuilder + .Entity() + .HasOne(e => e.Customer) + .WithMany(e => e.Orders) + .ExcludeFromMigrations()); + + AssertIsGeneric( + modelBuilder + .Entity() + .HasOne(e => e.Details) + .WithOne(e => e.Order) + .ExcludeFromMigrations()); } [ConditionalFact] @@ -1434,6 +1517,24 @@ public void Can_access_relationship() Assert.Equal("Splow", relationshipBuilder.Metadata.GetConstraintName()); } + [ConditionalFact] + public void Can_access_relationship_ExcludeFromMigrations() + { + var modelBuilder = CreateBuilder(); + var entityTypeBuilder = modelBuilder.Entity(typeof(Splot), ConfigurationSource.Convention); + entityTypeBuilder.Property(typeof(int), "Id", ConfigurationSource.Convention); + var relationshipBuilder = entityTypeBuilder.HasRelationship("Splot", ["Id"], ConfigurationSource.Convention); + + Assert.NotNull(relationshipBuilder.ExcludeFromMigrations(true)); + Assert.True(relationshipBuilder.Metadata.IsExcludedFromMigrations()); + + Assert.NotNull(relationshipBuilder.ExcludeFromMigrations(true, fromDataAnnotation: true)); + Assert.True(relationshipBuilder.Metadata.IsExcludedFromMigrations()); + + Assert.Null(relationshipBuilder.ExcludeFromMigrations(false)); + Assert.True(relationshipBuilder.Metadata.IsExcludedFromMigrations()); + } + private void AssertIsGeneric(EntityTypeBuilder _) { } diff --git a/test/EFCore.Relational.Tests/Extensions/RelationalMetadataExtensionsTest.cs b/test/EFCore.Relational.Tests/Extensions/RelationalMetadataExtensionsTest.cs index efa6a48d9d3..08b1c3f4358 100644 --- a/test/EFCore.Relational.Tests/Extensions/RelationalMetadataExtensionsTest.cs +++ b/test/EFCore.Relational.Tests/Extensions/RelationalMetadataExtensionsTest.cs @@ -347,6 +347,37 @@ public void Can_get_and_set_column_foreign_key_name() Assert.Equal("FK_Order_Customer_CustomerId", foreignKey.GetConstraintName()); } + [ConditionalFact] + public void Can_get_and_set_foreign_key_excluded_from_migrations() + { + var modelBuilder = new ModelBuilder(); + + modelBuilder + .Entity() + .HasKey(e => e.Id); + + var foreignKey = modelBuilder + .Entity() + .HasOne() + .WithOne() + .HasForeignKey(e => e.CustomerId) + .Metadata; + + Assert.False(foreignKey.IsExcludedFromMigrations()); + + foreignKey.SetIsExcludedFromMigrations(true); + + Assert.True(foreignKey.IsExcludedFromMigrations()); + + foreignKey.SetIsExcludedFromMigrations(false); + + Assert.False(foreignKey.IsExcludedFromMigrations()); + + foreignKey.SetIsExcludedFromMigrations(null); + + Assert.False(foreignKey.IsExcludedFromMigrations()); + } + [ConditionalFact] public void Can_get_and_set_index_name() { diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index 0eb2ae524f5..5a0ea588aee 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs @@ -1593,6 +1593,24 @@ public virtual void Detects_duplicate_foreignKey_names_within_hierarchy_with_dif modelBuilder); } + [ConditionalFact] + public virtual void Detects_duplicate_foreignKey_names_within_hierarchy_with_different_excluded_from_migrations() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(); + modelBuilder.Entity().HasOne().WithMany().HasForeignKey(c => c.Name).HasPrincipalKey(p => p.Name) + .HasConstraintName("FK_Animal_Person_Name").ExcludeFromMigrations(); + modelBuilder.Entity().HasOne().WithMany().HasForeignKey(d => d.Name).HasPrincipalKey(p => p.Name) + .HasConstraintName("FK_Animal_Person_Name"); + + VerifyError( + RelationalStrings.DuplicateForeignKeyExcludedFromMigrationsMismatch( + "{'" + nameof(Dog.Name) + "'}", nameof(Dog), + "{'" + nameof(Cat.Name) + "'}", nameof(Cat), + nameof(Animal), "FK_Animal_Person_Name"), + modelBuilder); + } + [ConditionalFact] public virtual void Passes_for_incompatible_foreignKeys_within_hierarchy() { diff --git a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs index 161b04c4483..5b53dbcc75b 100644 --- a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs +++ b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs @@ -3573,6 +3573,55 @@ public void Add_foreign_key() Assert.Equal(ReferentialAction.NoAction, addFkOperation.OnUpdate); }); + [ConditionalFact] + public void Add_foreign_key_excluded_from_migrations() + => Execute( + common => common.Entity( + "Amoeba", + x => + { + x.ToTable("Amoeba", "dbo"); + x.Property("Id"); + x.Property("ParentId"); + }), + _ => { }, + target => target.Entity( + "Amoeba", + x => x.HasOne("Amoeba").WithMany().HasForeignKey("ParentId").ExcludeFromMigrations() + ), + operations => + { + var createIndexOperation = Assert.IsType(Assert.Single(operations)); + Assert.Equal("dbo", createIndexOperation.Schema); + Assert.Equal("Amoeba", createIndexOperation.Table); + Assert.Equal("IX_Amoeba_ParentId", createIndexOperation.Name); + Assert.Equal(new[] { "ParentId" }, createIndexOperation.Columns); + }); + + [ConditionalFact] + public void Remove_foreign_key_excluded_from_migrations() + => Execute( + common => common.Entity( + "Amoeba", + x => + { + x.ToTable("Amoeba", "dbo"); + x.Property("Id"); + x.Property("ParentId"); + }), + source => source.Entity( + "Amoeba", + x => x.HasOne("Amoeba").WithMany().HasForeignKey("ParentId").ExcludeFromMigrations() + ), + _ => { }, + operations => + { + var dropIndexOperation = Assert.IsType(Assert.Single(operations)); + Assert.Equal("dbo", dropIndexOperation.Schema); + Assert.Equal("Amoeba", dropIndexOperation.Table); + Assert.Equal("IX_Amoeba_ParentId", dropIndexOperation.Name); + }); + [ConditionalFact] public void Add_optional_foreign_key() => Execute( diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs index 8f8dd721554..7385d49470e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs @@ -3588,6 +3588,16 @@ public override async Task Drop_foreign_key() """); } + public override async Task Add_foreign_key_excluded_from_migrations() + { + await base.Add_foreign_key_excluded_from_migrations(); + + AssertSql( + """ +CREATE INDEX [IX_Orders_CustomerId] ON [Orders] ([CustomerId]); +"""); + } + public override async Task Add_unique_constraint() { await base.Add_unique_constraint(); diff --git a/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs index 486fe39f437..83921a5a30d 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs @@ -1623,6 +1623,16 @@ public override async Task Drop_foreign_key() """); } + public override async Task Add_foreign_key_excluded_from_migrations() + { + await base.Add_foreign_key_excluded_from_migrations(); + + AssertSql( + """ +CREATE INDEX "IX_Orders_CustomerId" ON "Orders" ("CustomerId"); +"""); + } + public override async Task Add_unique_constraint() { await base.Add_unique_constraint(); From 8f456f5b5014dcad866a7f983f182de3ddcbf414 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Wed, 4 Mar 2026 20:52:02 +0200 Subject: [PATCH 2/4] Address review comments --- .../Design/AnnotationCodeGenerator.cs | 2 +- ...nalCSharpRuntimeAnnotationCodeGenerator.cs | 1 - .../RelationalForeignKeyBuilderExtensions.cs | 24 ++-- .../RelationalRuntimeModelConvention.cs | 1 - .../Internal/MigrationsModelDiffer.cs | 1 + ...rpMigrationsGeneratorTest.ModelSnapshot.cs | 4 +- .../Migrations/MigrationsTestBase.cs | 2 +- .../RelationalModelBuilderTest.cs | 111 +++++++++++++++++- .../RelationalTestModelBuilderExtensions.cs | 32 +++++ .../CompiledModelRelationalTestBase.cs | 5 + .../Design/AnnotationCodeGeneratorTest.cs | 6 +- .../RelationalBuilderExtensionsTest.cs | 91 +------------- .../RelationalModelValidatorTest.cs | 2 +- .../Internal/MigrationsModelDifferTest.cs | 60 +++++++++- .../BigModel/DependentBaseEntityType.cs | 1 + .../DependentBaseEntityType.cs | 1 + .../BigModel/DependentBaseEntityType.cs | 1 + .../DependentBaseEntityType.cs | 1 + 18 files changed, 232 insertions(+), 114 deletions(-) diff --git a/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs index 8f1abadd947..7fdac56089c 100644 --- a/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs +++ b/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs @@ -438,7 +438,7 @@ public virtual IReadOnlyList GenerateFluentApiCalls( GenerateSimpleFluentApiCall( annotations, RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations, - nameof(RelationalForeignKeyBuilderExtensions.ExcludeFromMigrations), + nameof(RelationalForeignKeyBuilderExtensions.ExcludeForeignKeyFromMigrations), methodCallCodeFragments); methodCallCodeFragments.AddRange(GenerateFluentApiCallsHelper(foreignKey, annotations, GenerateFluentApi)); diff --git a/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs index 0f3435229c4..ed97fd13171 100644 --- a/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs +++ b/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs @@ -2167,7 +2167,6 @@ public override void Generate(IForeignKey foreignKey, CSharpRuntimeAnnotationCod if (parameters.IsRuntime) { parameters.Annotations.Remove(RelationalAnnotationNames.ForeignKeyMappings); - parameters.Annotations.Remove(RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations); } base.Generate(foreignKey, parameters); diff --git a/src/EFCore.Relational/Extensions/RelationalForeignKeyBuilderExtensions.cs b/src/EFCore.Relational/Extensions/RelationalForeignKeyBuilderExtensions.cs index 6b05406bd48..276d2e75fb0 100644 --- a/src/EFCore.Relational/Extensions/RelationalForeignKeyBuilderExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalForeignKeyBuilderExtensions.cs @@ -184,7 +184,7 @@ public static bool CanSetConstraintName( /// The builder being used to configure the relationship. /// A value indicating whether the foreign key constraint is excluded from migrations. /// The same builder instance so that multiple calls can be chained. - public static ReferenceCollectionBuilder ExcludeFromMigrations( + public static ReferenceCollectionBuilder ExcludeForeignKeyFromMigrations( this ReferenceCollectionBuilder referenceCollectionBuilder, bool excluded = true) { @@ -205,12 +205,12 @@ public static ReferenceCollectionBuilder ExcludeFromMigrations( /// The same builder instance so that multiple calls can be chained. /// The principal entity type in this relationship. /// The dependent entity type in this relationship. - public static ReferenceCollectionBuilder ExcludeFromMigrations( + public static ReferenceCollectionBuilder ExcludeForeignKeyFromMigrations( this ReferenceCollectionBuilder referenceCollectionBuilder, bool excluded = true) where TEntity : class where TRelatedEntity : class - => (ReferenceCollectionBuilder)ExcludeFromMigrations( + => (ReferenceCollectionBuilder)ExcludeForeignKeyFromMigrations( (ReferenceCollectionBuilder)referenceCollectionBuilder, excluded); /// @@ -223,7 +223,7 @@ public static ReferenceCollectionBuilder ExcludeFromMig /// The builder being used to configure the relationship. /// A value indicating whether the foreign key constraint is excluded from migrations. /// The same builder instance so that multiple calls can be chained. - public static ReferenceReferenceBuilder ExcludeFromMigrations( + public static ReferenceReferenceBuilder ExcludeForeignKeyFromMigrations( this ReferenceReferenceBuilder referenceReferenceBuilder, bool excluded = true) { @@ -244,12 +244,12 @@ public static ReferenceReferenceBuilder ExcludeFromMigrations( /// The same builder instance so that multiple calls can be chained. /// The entity type on one end of the relationship. /// The entity type on the other end of the relationship. - public static ReferenceReferenceBuilder ExcludeFromMigrations( + public static ReferenceReferenceBuilder ExcludeForeignKeyFromMigrations( this ReferenceReferenceBuilder referenceReferenceBuilder, bool excluded = true) where TEntity : class where TRelatedEntity : class - => (ReferenceReferenceBuilder)ExcludeFromMigrations( + => (ReferenceReferenceBuilder)ExcludeForeignKeyFromMigrations( (ReferenceReferenceBuilder)referenceReferenceBuilder, excluded); /// @@ -262,7 +262,7 @@ public static ReferenceReferenceBuilder ExcludeFromMigr /// The builder being used to configure the relationship. /// A value indicating whether the foreign key constraint is excluded from migrations. /// The same builder instance so that multiple calls can be chained. - public static OwnershipBuilder ExcludeFromMigrations( + public static OwnershipBuilder ExcludeForeignKeyFromMigrations( this OwnershipBuilder ownershipBuilder, bool excluded = true) { @@ -283,12 +283,12 @@ public static OwnershipBuilder ExcludeFromMigrations( /// The same builder instance so that multiple calls can be chained. /// The entity type on one end of the relationship. /// The entity type on the other end of the relationship. - public static OwnershipBuilder ExcludeFromMigrations( + public static OwnershipBuilder ExcludeForeignKeyFromMigrations( this OwnershipBuilder ownershipBuilder, bool excluded = true) where TEntity : class where TDependentEntity : class - => (OwnershipBuilder)ExcludeFromMigrations( + => (OwnershipBuilder)ExcludeForeignKeyFromMigrations( (OwnershipBuilder)ownershipBuilder, excluded); /// @@ -305,12 +305,12 @@ public static OwnershipBuilder ExcludeFromMigrations< /// The same builder instance if the configuration was applied, /// otherwise. /// - public static IConventionForeignKeyBuilder? ExcludeFromMigrations( + public static IConventionForeignKeyBuilder? ExcludeForeignKeyFromMigrations( this IConventionForeignKeyBuilder relationship, bool? excluded, bool fromDataAnnotation = false) { - if (!relationship.CanSetExcludeFromMigrations(excluded, fromDataAnnotation)) + if (!relationship.CanSetExcludeForeignKeyFromMigrations(excluded, fromDataAnnotation)) { return null; } @@ -330,7 +330,7 @@ public static OwnershipBuilder ExcludeFromMigrations< /// A value indicating whether the foreign key constraint is excluded from migrations. /// Indicates whether the configuration was specified using a data annotation. /// if the configuration can be applied. - public static bool CanSetExcludeFromMigrations( + public static bool CanSetExcludeForeignKeyFromMigrations( this IConventionForeignKeyBuilder relationship, bool? excluded, bool fromDataAnnotation = false) diff --git a/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs b/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs index 5a31e791746..20aeb33cd38 100644 --- a/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs @@ -505,7 +505,6 @@ protected override void ProcessForeignKeyAnnotations( if (runtime) { annotations.Remove(RelationalAnnotationNames.ForeignKeyMappings); - annotations.Remove(RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations); } } diff --git a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs index d72ec524220..91d50924a0e 100644 --- a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs +++ b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs @@ -1400,6 +1400,7 @@ protected virtual IEnumerable Diff( Add, Remove, (s, t, context) => s.Name == t.Name + && s.IsExcludedFromMigrations == t.IsExcludedFromMigrations && s.Columns.Select(c => c.Name).SequenceEqual( t.Columns.Select(c => context.FindSource(c)?.Name)) && s.PrincipalTable == context.FindSource(t.PrincipalTable) diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs index 469d99cf3e7..145e0c2fd26 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs @@ -8026,7 +8026,7 @@ public virtual void ForeignKey_excluded_from_migrations_is_stored_in_snapshot() .HasOne(e => e.EntityWithOneProperty) .WithOne(e => e.EntityWithTwoProperties) .HasForeignKey(e => e.AlternateId) - .ExcludeFromMigrations(); + .ExcludeForeignKeyFromMigrations(); }, AddBoilerPlate( GetHeading() @@ -8070,7 +8070,7 @@ public virtual void ForeignKey_excluded_from_migrations_is_stored_in_snapshot() .HasForeignKey("Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorTest+EntityWithTwoProperties", "AlternateId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() - .ExcludeFromMigrations(true); + .ExcludeForeignKeyFromMigrations(true); b.Navigation("EntityWithOneProperty"); }); diff --git a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs index 43a6e35d9aa..1ff3b12810d 100644 --- a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs @@ -2348,7 +2348,7 @@ public virtual Task Add_foreign_key_excluded_from_migrations() }, builder => { }, builder => builder.Entity("Orders").HasOne("Customers").WithMany() - .HasForeignKey("CustomerId").ExcludeFromMigrations(), + .HasForeignKey("CustomerId").ExcludeForeignKeyFromMigrations(), model => { var ordersTable = Assert.Single(model.Tables, t => t.Name == "Orders"); diff --git a/test/EFCore.Relational.Specification.Tests/ModelBuilding/RelationalModelBuilderTest.cs b/test/EFCore.Relational.Specification.Tests/ModelBuilding/RelationalModelBuilderTest.cs index fca2fceb55a..cffd0c597ab 100644 --- a/test/EFCore.Relational.Specification.Tests/ModelBuilding/RelationalModelBuilderTest.cs +++ b/test/EFCore.Relational.Specification.Tests/ModelBuilding/RelationalModelBuilderTest.cs @@ -996,14 +996,92 @@ public virtual void Can_use_table_splitting() } } - public abstract class RelationalOneToManyTestBase(RelationalModelBuilderFixture fixture) : OneToManyTestBase(fixture); + public abstract class RelationalOneToManyTestBase(RelationalModelBuilderFixture fixture) : OneToManyTestBase(fixture) + { + [ConditionalFact] + public virtual void Can_exclude_foreign_key_from_migrations_for_one_to_many() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Entity().HasMany(e => e.Orders).WithOne(e => e.Customer) + .ExcludeForeignKeyFromMigrations(); + + var model = modelBuilder.FinalizeModel(); + + var foreignKey = model.FindEntityType(typeof(Order))!.GetForeignKeys() + .Single(fk => fk.PrincipalEntityType.ClrType == typeof(Customer)); - public abstract class RelationalManyToOneTestBase(RelationalModelBuilderFixture fixture) : ManyToOneTestBase(fixture); + Assert.True(foreignKey.IsExcludedFromMigrations()); + } + } - public abstract class RelationalOneToOneTestBase(RelationalModelBuilderFixture fixture) : OneToOneTestBase(fixture); + public abstract class RelationalManyToOneTestBase(RelationalModelBuilderFixture fixture) : ManyToOneTestBase(fixture) + { + [ConditionalFact] + public virtual void Can_exclude_foreign_key_from_migrations_for_many_to_one() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Entity().HasOne(e => e.Customer).WithMany(e => e.Orders) + .ExcludeForeignKeyFromMigrations(); + + var model = modelBuilder.FinalizeModel(); + + var foreignKey = model.FindEntityType(typeof(Order))!.GetForeignKeys() + .Single(fk => fk.PrincipalEntityType.ClrType == typeof(Customer)); + + Assert.True(foreignKey.IsExcludedFromMigrations()); + } + } + + public abstract class RelationalOneToOneTestBase(RelationalModelBuilderFixture fixture) : OneToOneTestBase(fixture) + { + [ConditionalFact] + public virtual void Can_exclude_foreign_key_from_migrations_for_one_to_one() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Entity().HasOne(e => e.Details).WithOne(e => e.Order) + .HasPrincipalKey(e => e.OrderId) + .ExcludeForeignKeyFromMigrations(); + + var model = modelBuilder.FinalizeModel(); + + var foreignKey = model.FindEntityType(typeof(OrderDetails))!.GetForeignKeys().Single(); + + Assert.True(foreignKey.IsExcludedFromMigrations()); + } + } public abstract class RelationalManyToManyTestBase(RelationalModelBuilderFixture fixture) : ManyToManyTestBase(fixture) { + [ConditionalFact] + public virtual void Can_exclude_foreign_key_from_migrations_for_many_to_many() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Ignore(); + + modelBuilder.Entity() + .HasMany(e => e.Categories) + .WithMany(e => e.Products) + .UsingEntity( + right => right.HasOne().WithMany().ExcludeForeignKeyFromMigrations(), + left => left.HasOne().WithMany().ExcludeForeignKeyFromMigrations()); + + var model = modelBuilder.FinalizeModel(); + + var joinEntityType = model.GetEntityTypes() + .Single(e => e.ClrType == typeof(Dictionary) && e.Name.Contains("Category")); + foreach (var foreignKey in joinEntityType.GetForeignKeys()) + { + Assert.True(foreignKey.IsExcludedFromMigrations()); + } + } + [ConditionalFact] // Issue #27990 public virtual void Can_use_ForeignKeyAttribute_with_InversePropertyAttribute() { @@ -1142,6 +1220,33 @@ protected class MotorBauart public abstract class RelationalOwnedTypesTestBase(RelationalModelBuilderFixture fixture) : OwnedTypesTestBase(fixture) { + [ConditionalFact] + public virtual void Can_exclude_foreign_key_from_migrations_for_owned_type() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Ignore(); + modelBuilder.Ignore(); + modelBuilder.Ignore(); + + modelBuilder.Entity().OwnsOne( + b => b.Label, lb => + { + lb.Ignore(l => l.Book); + lb.WithOwner().ExcludeForeignKeyFromMigrations(); + }); + modelBuilder.Entity() + .OwnsOne(b => b.AlternateLabel); + + var model = modelBuilder.FinalizeModel(); + + var ownedType = model.FindEntityType(typeof(BookLabel), nameof(Book.Label), model.FindEntityType(typeof(Book))!)!; + var foreignKey = ownedType.GetForeignKeys().Single(); + + Assert.True(foreignKey.IsOwnership); + Assert.True(foreignKey.IsExcludedFromMigrations()); + } + [ConditionalFact] public virtual void Can_use_table_splitting_with_owned_reference() { diff --git a/test/EFCore.Relational.Specification.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs b/test/EFCore.Relational.Specification.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs index d2e08888ee2..fcb7e2555f7 100644 --- a/test/EFCore.Relational.Specification.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs +++ b/test/EFCore.Relational.Specification.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs @@ -1605,6 +1605,38 @@ public static ModelBuilderTest.TestReferenceCollectionBuilder ExcludeForeignKeyFromMigrations( + this ModelBuilderTest.TestOwnershipBuilder builder, + bool excluded = true) + where TOwnerEntity : class + where TDependentEntity : class + { + builder.Metadata.SetIsExcludedFromMigrations(excluded); + return builder; + } + + public static ModelBuilderTest.TestReferenceReferenceBuilder ExcludeForeignKeyFromMigrations + ( + this ModelBuilderTest.TestReferenceReferenceBuilder builder, + bool excluded = true) + where TOwnerEntity : class + where TDependentEntity : class + { + builder.Metadata.SetIsExcludedFromMigrations(excluded); + return builder; + } + + public static ModelBuilderTest.TestReferenceCollectionBuilder ExcludeForeignKeyFromMigrations + ( + this ModelBuilderTest.TestReferenceCollectionBuilder builder, + bool excluded = true) + where TOwnerEntity : class + where TDependentEntity : class + { + builder.Metadata.SetIsExcludedFromMigrations(excluded); + return builder; + } + public static ModelBuilderTest.TestIndexBuilder HasFilter( this ModelBuilderTest.TestIndexBuilder builder, string? filterExpression) diff --git a/test/EFCore.Relational.Specification.Tests/Scaffolding/CompiledModelRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Scaffolding/CompiledModelRelationalTestBase.cs index 894a8dcf14f..dd2b30777aa 100644 --- a/test/EFCore.Relational.Specification.Tests/Scaffolding/CompiledModelRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Scaffolding/CompiledModelRelationalTestBase.cs @@ -81,6 +81,10 @@ protected override void BuildBigModel(ModelBuilder modelBuilder, bool jsonColumn } }); + eb.HasOne(e => e.Dependent).WithOne(e => e.Principal) + .HasForeignKey>() + .ExcludeForeignKeyFromMigrations(); + eb.HasMany(e => e.Principals).WithMany(e => (ICollection>>)e.Deriveds) .UsingEntity(jb => { @@ -213,6 +217,7 @@ protected override void AssertBigModel(IModel model, bool jsonColumns) var dependentNavigation = principalDerived.GetDeclaredNavigations().First(); var dependentForeignKey = dependentNavigation.ForeignKey; + Assert.True(dependentForeignKey.IsExcludedFromMigrations()); var referenceOwnedNavigation = principalBase.GetNavigations().Single(); var referenceOwnedType = referenceOwnedNavigation.TargetEntityType; diff --git a/test/EFCore.Relational.Tests/Design/AnnotationCodeGeneratorTest.cs b/test/EFCore.Relational.Tests/Design/AnnotationCodeGeneratorTest.cs index 7ac8d15fc2b..9d9f78bb052 100644 --- a/test/EFCore.Relational.Tests/Design/AnnotationCodeGeneratorTest.cs +++ b/test/EFCore.Relational.Tests/Design/AnnotationCodeGeneratorTest.cs @@ -63,21 +63,21 @@ public void IsForeignKeyExcludedFromMigrations_false_is_handled_by_convention() } [ConditionalFact] - public void GenerateFluentApi_IForeignKey_works_with_ExcludeFromMigrations() + public void GenerateFluentApi_IForeignKey_works_with_ExcludeForeignKeyFromMigrations() { var modelBuilder = CreateModelBuilder(); modelBuilder.Entity("Blog", x => { x.Property("Id"); x.Property("ParentId"); - x.HasOne("Blog").WithMany().HasForeignKey("ParentId").ExcludeFromMigrations(); + x.HasOne("Blog").WithMany().HasForeignKey("ParentId").ExcludeForeignKeyFromMigrations(); }); var foreignKey = modelBuilder.Model.FindEntityType("Blog").GetForeignKeys().Single(); var annotations = foreignKey.GetAnnotations().ToDictionary(a => a.Name, a => a); var result = CreateGenerator().GenerateFluentApiCalls((IForeignKey)foreignKey, annotations).Single(); - Assert.Equal("ExcludeFromMigrations", result.Method); + Assert.Equal("ExcludeForeignKeyFromMigrations", result.Method); Assert.Equal(true, Assert.Single(result.Arguments)); } diff --git a/test/EFCore.Relational.Tests/Extensions/RelationalBuilderExtensionsTest.cs b/test/EFCore.Relational.Tests/Extensions/RelationalBuilderExtensionsTest.cs index 28497b96471..b331c45608d 100644 --- a/test/EFCore.Relational.Tests/Extensions/RelationalBuilderExtensionsTest.cs +++ b/test/EFCore.Relational.Tests/Extensions/RelationalBuilderExtensionsTest.cs @@ -372,69 +372,6 @@ public void Can_set_foreign_key_name_for_one_to_one_with_FK_specified() Assert.Equal("LemonSupreme", foreignKey.GetConstraintName()); } - [ConditionalFact] - public void Can_set_foreign_key_exclude_from_migrations_for_one_to_many() - { - var modelBuilder = CreateConventionModelBuilder(); - - modelBuilder - .Entity().HasMany(e => e.Orders).WithOne(e => e.Customer) - .ExcludeFromMigrations(); - - var foreignKey = modelBuilder.Model.FindEntityType(typeof(Order)).GetForeignKeys() - .Single(fk => fk.PrincipalEntityType.ClrType == typeof(Customer)); - - Assert.True(foreignKey.IsExcludedFromMigrations()); - - modelBuilder - .Entity().HasMany(e => e.Orders).WithOne(e => e.Customer) - .ExcludeFromMigrations(false); - - Assert.False(foreignKey.IsExcludedFromMigrations()); - } - - [ConditionalFact] - public void Can_set_foreign_key_exclude_from_migrations_for_many_to_one() - { - var modelBuilder = CreateConventionModelBuilder(); - - modelBuilder - .Entity().HasOne(e => e.Customer).WithMany(e => e.Orders) - .ExcludeFromMigrations(); - - var foreignKey = modelBuilder.Model.FindEntityType(typeof(Order)).GetForeignKeys() - .Single(fk => fk.PrincipalEntityType.ClrType == typeof(Customer)); - - Assert.True(foreignKey.IsExcludedFromMigrations()); - - modelBuilder - .Entity().HasOne(e => e.Customer).WithMany(e => e.Orders) - .ExcludeFromMigrations(false); - - Assert.False(foreignKey.IsExcludedFromMigrations()); - } - - [ConditionalFact] - public void Can_set_foreign_key_exclude_from_migrations_for_one_to_one() - { - var modelBuilder = CreateConventionModelBuilder(); - - modelBuilder - .Entity().HasOne(e => e.Details).WithOne(e => e.Order) - .HasPrincipalKey(e => e.OrderId) - .ExcludeFromMigrations(); - - var foreignKey = modelBuilder.Model.FindEntityType(typeof(OrderDetails)).GetForeignKeys().Single(); - - Assert.True(foreignKey.IsExcludedFromMigrations()); - - modelBuilder - .Entity().HasOne(e => e.Details).WithOne(e => e.Order) - .ExcludeFromMigrations(false); - - Assert.False(foreignKey.IsExcludedFromMigrations()); - } - [ConditionalFact] public void Can_access_index() { @@ -1454,26 +1391,6 @@ public void Relational_relationship_methods_dont_break_out_of_the_generics() .HasOne(e => e.Details) .WithOne(e => e.Order) .HasConstraintName("Simon")); - - AssertIsGeneric( - modelBuilder - .Entity().HasMany(e => e.Orders) - .WithOne(e => e.Customer) - .ExcludeFromMigrations()); - - AssertIsGeneric( - modelBuilder - .Entity() - .HasOne(e => e.Customer) - .WithMany(e => e.Orders) - .ExcludeFromMigrations()); - - AssertIsGeneric( - modelBuilder - .Entity() - .HasOne(e => e.Details) - .WithOne(e => e.Order) - .ExcludeFromMigrations()); } [ConditionalFact] @@ -1518,20 +1435,20 @@ public void Can_access_relationship() } [ConditionalFact] - public void Can_access_relationship_ExcludeFromMigrations() + public void Can_access_relationship_ExcludeForeignKeyFromMigrations() { var modelBuilder = CreateBuilder(); var entityTypeBuilder = modelBuilder.Entity(typeof(Splot), ConfigurationSource.Convention); entityTypeBuilder.Property(typeof(int), "Id", ConfigurationSource.Convention); var relationshipBuilder = entityTypeBuilder.HasRelationship("Splot", ["Id"], ConfigurationSource.Convention); - Assert.NotNull(relationshipBuilder.ExcludeFromMigrations(true)); + Assert.NotNull(relationshipBuilder.ExcludeForeignKeyFromMigrations(true)); Assert.True(relationshipBuilder.Metadata.IsExcludedFromMigrations()); - Assert.NotNull(relationshipBuilder.ExcludeFromMigrations(true, fromDataAnnotation: true)); + Assert.NotNull(relationshipBuilder.ExcludeForeignKeyFromMigrations(true, fromDataAnnotation: true)); Assert.True(relationshipBuilder.Metadata.IsExcludedFromMigrations()); - Assert.Null(relationshipBuilder.ExcludeFromMigrations(false)); + Assert.Null(relationshipBuilder.ExcludeForeignKeyFromMigrations(false)); Assert.True(relationshipBuilder.Metadata.IsExcludedFromMigrations()); } diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index 5a0ea588aee..7c1a2462f8d 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs @@ -1599,7 +1599,7 @@ public virtual void Detects_duplicate_foreignKey_names_within_hierarchy_with_dif var modelBuilder = CreateConventionModelBuilder(); modelBuilder.Entity(); modelBuilder.Entity().HasOne().WithMany().HasForeignKey(c => c.Name).HasPrincipalKey(p => p.Name) - .HasConstraintName("FK_Animal_Person_Name").ExcludeFromMigrations(); + .HasConstraintName("FK_Animal_Person_Name").ExcludeForeignKeyFromMigrations(); modelBuilder.Entity().HasOne().WithMany().HasForeignKey(d => d.Name).HasPrincipalKey(p => p.Name) .HasConstraintName("FK_Animal_Person_Name"); diff --git a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs index 5b53dbcc75b..96f9cd59dec 100644 --- a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs +++ b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs @@ -3587,7 +3587,7 @@ public void Add_foreign_key_excluded_from_migrations() _ => { }, target => target.Entity( "Amoeba", - x => x.HasOne("Amoeba").WithMany().HasForeignKey("ParentId").ExcludeFromMigrations() + x => x.HasOne("Amoeba").WithMany().HasForeignKey("ParentId").ExcludeForeignKeyFromMigrations() ), operations => { @@ -3611,7 +3611,7 @@ public void Remove_foreign_key_excluded_from_migrations() }), source => source.Entity( "Amoeba", - x => x.HasOne("Amoeba").WithMany().HasForeignKey("ParentId").ExcludeFromMigrations() + x => x.HasOne("Amoeba").WithMany().HasForeignKey("ParentId").ExcludeForeignKeyFromMigrations() ), _ => { }, operations => @@ -3622,6 +3622,62 @@ public void Remove_foreign_key_excluded_from_migrations() Assert.Equal("IX_Amoeba_ParentId", dropIndexOperation.Name); }); + [ConditionalFact] + public void Exclude_existing_foreign_key_from_migrations() + => Execute( + common => common.Entity( + "Amoeba", + x => + { + x.ToTable("Amoeba", "dbo"); + x.Property("Id"); + x.Property("ParentId"); + }), + source => source.Entity( + "Amoeba", + x => x.HasOne("Amoeba").WithMany().HasForeignKey("ParentId") + ), + target => target.Entity( + "Amoeba", + x => x.HasOne("Amoeba").WithMany().HasForeignKey("ParentId").ExcludeForeignKeyFromMigrations() + ), + operations => + { + var dropFkOperation = Assert.IsType(Assert.Single(operations)); + Assert.Equal("dbo", dropFkOperation.Schema); + Assert.Equal("Amoeba", dropFkOperation.Table); + Assert.Equal("FK_Amoeba_Amoeba_ParentId", dropFkOperation.Name); + }); + + [ConditionalFact] + public void Exclude_existing_foreign_key_from_migrations_and_change_delete_behavior() + => Execute( + common => common.Entity( + "Amoeba", + x => + { + x.ToTable("Amoeba", "dbo"); + x.Property("Id"); + x.Property("ParentId"); + }), + source => source.Entity( + "Amoeba", + x => x.HasOne("Amoeba").WithMany().HasForeignKey("ParentId").OnDelete(DeleteBehavior.Restrict) + ), + target => target.Entity( + "Amoeba", + x => x.HasOne("Amoeba").WithMany().HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Cascade) + .ExcludeForeignKeyFromMigrations() + ), + operations => + { + var dropFkOperation = Assert.IsType(Assert.Single(operations)); + Assert.Equal("dbo", dropFkOperation.Schema); + Assert.Equal("Amoeba", dropFkOperation.Table); + Assert.Equal("FK_Amoeba_Amoeba_ParentId", dropFkOperation.Name); + }); + [ConditionalFact] public void Add_optional_foreign_key() => Execute( diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel/DependentBaseEntityType.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel/DependentBaseEntityType.cs index 1db8e08605c..99162e1de23 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel/DependentBaseEntityType.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel/DependentBaseEntityType.cs @@ -293,6 +293,7 @@ public static RuntimeForeignKey CreateForeignKey2(RuntimeEntityType declaringEnt shadowIndex: -1, relationshipIndex: 4, storeGenerationIndex: -1); + runtimeForeignKey.AddAnnotation("Relational:IsForeignKeyExcludedFromMigrations", true); return runtimeForeignKey; } diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/DependentBaseEntityType.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/DependentBaseEntityType.cs index 1db8e08605c..99162e1de23 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/DependentBaseEntityType.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/DependentBaseEntityType.cs @@ -293,6 +293,7 @@ public static RuntimeForeignKey CreateForeignKey2(RuntimeEntityType declaringEnt shadowIndex: -1, relationshipIndex: 4, storeGenerationIndex: -1); + runtimeForeignKey.AddAnnotation("Relational:IsForeignKeyExcludedFromMigrations", true); return runtimeForeignKey; } diff --git a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel/DependentBaseEntityType.cs b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel/DependentBaseEntityType.cs index 02cb12eb7dc..353093451dc 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel/DependentBaseEntityType.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel/DependentBaseEntityType.cs @@ -281,6 +281,7 @@ public static RuntimeForeignKey CreateForeignKey2(RuntimeEntityType declaringEnt shadowIndex: -1, relationshipIndex: 4, storeGenerationIndex: -1); + runtimeForeignKey.AddAnnotation("Relational:IsForeignKeyExcludedFromMigrations", true); return runtimeForeignKey; } diff --git a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/DependentBaseEntityType.cs b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/DependentBaseEntityType.cs index 02cb12eb7dc..353093451dc 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/DependentBaseEntityType.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/DependentBaseEntityType.cs @@ -281,6 +281,7 @@ public static RuntimeForeignKey CreateForeignKey2(RuntimeEntityType declaringEnt shadowIndex: -1, relationshipIndex: 4, storeGenerationIndex: -1); + runtimeForeignKey.AddAnnotation("Relational:IsForeignKeyExcludedFromMigrations", true); return runtimeForeignKey; } From 6694b276a37eb08f980ccae768e9feb0732c4aa3 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Thu, 5 Mar 2026 00:11:55 +0200 Subject: [PATCH 3/4] Additional fix --- .../Baselines/No_NativeAOT/DependentBaseEntityType.cs | 1 + .../Baselines/No_NativeAOT/DependentBaseEntityType.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/No_NativeAOT/DependentBaseEntityType.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/No_NativeAOT/DependentBaseEntityType.cs index a856329d746..fb66cc60c8e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/No_NativeAOT/DependentBaseEntityType.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/No_NativeAOT/DependentBaseEntityType.cs @@ -108,6 +108,7 @@ public static RuntimeForeignKey CreateForeignKey2(RuntimeEntityType declaringEnt eagerLoaded: true, lazyLoadingEnabled: false); + runtimeForeignKey.AddAnnotation("Relational:IsForeignKeyExcludedFromMigrations", true); return runtimeForeignKey; } diff --git a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/No_NativeAOT/DependentBaseEntityType.cs b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/No_NativeAOT/DependentBaseEntityType.cs index 5f4687b055c..e400fc440a2 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/No_NativeAOT/DependentBaseEntityType.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/No_NativeAOT/DependentBaseEntityType.cs @@ -104,6 +104,7 @@ public static RuntimeForeignKey CreateForeignKey2(RuntimeEntityType declaringEnt eagerLoaded: true, lazyLoadingEnabled: false); + runtimeForeignKey.AddAnnotation("Relational:IsForeignKeyExcludedFromMigrations", true); return runtimeForeignKey; } From 20a648e80773cc987b4beca71e27f6ae03334ad0 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Thu, 5 Mar 2026 00:34:59 +0200 Subject: [PATCH 4/4] Address review comments --- ...nalCSharpRuntimeAnnotationCodeGenerator.cs | 4 ++++ .../RelationalRuntimeModelConvention.cs | 4 ++++ .../RelationalTestModelBuilderExtensions.cs | 7 +++--- .../CompiledModelRelationalTestBase.cs | 2 +- .../Internal/MigrationsModelDifferTest.cs | 23 +++++++++++++++---- .../BigModel/DependentBaseEntityType.cs | 1 - .../DependentBaseEntityType.cs | 1 - .../No_NativeAOT/DependentBaseEntityType.cs | 1 - .../BigModel/DependentBaseEntityType.cs | 1 - .../DependentBaseEntityType.cs | 1 - .../No_NativeAOT/DependentBaseEntityType.cs | 1 - 11 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs index ed97fd13171..3ce5986f5d1 100644 --- a/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs +++ b/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs @@ -2168,6 +2168,10 @@ public override void Generate(IForeignKey foreignKey, CSharpRuntimeAnnotationCod { parameters.Annotations.Remove(RelationalAnnotationNames.ForeignKeyMappings); } + else + { + parameters.Annotations.Remove(RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations); + } base.Generate(foreignKey, parameters); } diff --git a/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs b/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs index 20aeb33cd38..dd07162f799 100644 --- a/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs @@ -506,6 +506,10 @@ protected override void ProcessForeignKeyAnnotations( { annotations.Remove(RelationalAnnotationNames.ForeignKeyMappings); } + else + { + annotations.Remove(RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations); + } } private RuntimeStoredProcedure Create(IStoredProcedure storedProcedure, RuntimeEntityType runtimeEntityType) diff --git a/test/EFCore.Relational.Specification.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs b/test/EFCore.Relational.Specification.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs index fcb7e2555f7..23e749090a7 100644 --- a/test/EFCore.Relational.Specification.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs +++ b/test/EFCore.Relational.Specification.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs @@ -1605,9 +1605,10 @@ public static ModelBuilderTest.TestReferenceCollectionBuilder ExcludeForeignKeyFromMigrations( - this ModelBuilderTest.TestOwnershipBuilder builder, - bool excluded = true) + public static ModelBuilderTest.TestOwnershipBuilder ExcludeForeignKeyFromMigrations + ( + this ModelBuilderTest.TestOwnershipBuilder builder, + bool excluded = true) where TOwnerEntity : class where TDependentEntity : class { diff --git a/test/EFCore.Relational.Specification.Tests/Scaffolding/CompiledModelRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Scaffolding/CompiledModelRelationalTestBase.cs index dd2b30777aa..897a77fbeb4 100644 --- a/test/EFCore.Relational.Specification.Tests/Scaffolding/CompiledModelRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Scaffolding/CompiledModelRelationalTestBase.cs @@ -217,7 +217,7 @@ protected override void AssertBigModel(IModel model, bool jsonColumns) var dependentNavigation = principalDerived.GetDeclaredNavigations().First(); var dependentForeignKey = dependentNavigation.ForeignKey; - Assert.True(dependentForeignKey.IsExcludedFromMigrations()); + Assert.False(dependentForeignKey.IsExcludedFromMigrations()); var referenceOwnedNavigation = principalBase.GetNavigations().Single(); var referenceOwnedType = referenceOwnedNavigation.TargetEntityType; diff --git a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs index 96f9cd59dec..aed949d96a0 100644 --- a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs +++ b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs @@ -3641,12 +3641,19 @@ public void Exclude_existing_foreign_key_from_migrations() "Amoeba", x => x.HasOne("Amoeba").WithMany().HasForeignKey("ParentId").ExcludeForeignKeyFromMigrations() ), - operations => + upOps => { - var dropFkOperation = Assert.IsType(Assert.Single(operations)); + var dropFkOperation = Assert.IsType(Assert.Single(upOps)); Assert.Equal("dbo", dropFkOperation.Schema); Assert.Equal("Amoeba", dropFkOperation.Table); Assert.Equal("FK_Amoeba_Amoeba_ParentId", dropFkOperation.Name); + }, + downOps => + { + var addFkOperation = Assert.IsType(Assert.Single(downOps)); + Assert.Equal("dbo", addFkOperation.Schema); + Assert.Equal("Amoeba", addFkOperation.Table); + Assert.Equal("FK_Amoeba_Amoeba_ParentId", addFkOperation.Name); }); [ConditionalFact] @@ -3670,12 +3677,20 @@ public void Exclude_existing_foreign_key_from_migrations_and_change_delete_behav .OnDelete(DeleteBehavior.Cascade) .ExcludeForeignKeyFromMigrations() ), - operations => + upOps => { - var dropFkOperation = Assert.IsType(Assert.Single(operations)); + var dropFkOperation = Assert.IsType(Assert.Single(upOps)); Assert.Equal("dbo", dropFkOperation.Schema); Assert.Equal("Amoeba", dropFkOperation.Table); Assert.Equal("FK_Amoeba_Amoeba_ParentId", dropFkOperation.Name); + }, + downOps => + { + var addFkOperation = Assert.IsType(Assert.Single(downOps)); + Assert.Equal("dbo", addFkOperation.Schema); + Assert.Equal("Amoeba", addFkOperation.Table); + Assert.Equal("FK_Amoeba_Amoeba_ParentId", addFkOperation.Name); + Assert.Equal(ReferentialAction.Restrict, addFkOperation.OnDelete); }); [ConditionalFact] diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel/DependentBaseEntityType.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel/DependentBaseEntityType.cs index 99162e1de23..1db8e08605c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel/DependentBaseEntityType.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel/DependentBaseEntityType.cs @@ -293,7 +293,6 @@ public static RuntimeForeignKey CreateForeignKey2(RuntimeEntityType declaringEnt shadowIndex: -1, relationshipIndex: 4, storeGenerationIndex: -1); - runtimeForeignKey.AddAnnotation("Relational:IsForeignKeyExcludedFromMigrations", true); return runtimeForeignKey; } diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/DependentBaseEntityType.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/DependentBaseEntityType.cs index 99162e1de23..1db8e08605c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/DependentBaseEntityType.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/DependentBaseEntityType.cs @@ -293,7 +293,6 @@ public static RuntimeForeignKey CreateForeignKey2(RuntimeEntityType declaringEnt shadowIndex: -1, relationshipIndex: 4, storeGenerationIndex: -1); - runtimeForeignKey.AddAnnotation("Relational:IsForeignKeyExcludedFromMigrations", true); return runtimeForeignKey; } diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/No_NativeAOT/DependentBaseEntityType.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/No_NativeAOT/DependentBaseEntityType.cs index fb66cc60c8e..a856329d746 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/No_NativeAOT/DependentBaseEntityType.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/No_NativeAOT/DependentBaseEntityType.cs @@ -108,7 +108,6 @@ public static RuntimeForeignKey CreateForeignKey2(RuntimeEntityType declaringEnt eagerLoaded: true, lazyLoadingEnabled: false); - runtimeForeignKey.AddAnnotation("Relational:IsForeignKeyExcludedFromMigrations", true); return runtimeForeignKey; } diff --git a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel/DependentBaseEntityType.cs b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel/DependentBaseEntityType.cs index 353093451dc..02cb12eb7dc 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel/DependentBaseEntityType.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel/DependentBaseEntityType.cs @@ -281,7 +281,6 @@ public static RuntimeForeignKey CreateForeignKey2(RuntimeEntityType declaringEnt shadowIndex: -1, relationshipIndex: 4, storeGenerationIndex: -1); - runtimeForeignKey.AddAnnotation("Relational:IsForeignKeyExcludedFromMigrations", true); return runtimeForeignKey; } diff --git a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/DependentBaseEntityType.cs b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/DependentBaseEntityType.cs index 353093451dc..02cb12eb7dc 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/DependentBaseEntityType.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/DependentBaseEntityType.cs @@ -281,7 +281,6 @@ public static RuntimeForeignKey CreateForeignKey2(RuntimeEntityType declaringEnt shadowIndex: -1, relationshipIndex: 4, storeGenerationIndex: -1); - runtimeForeignKey.AddAnnotation("Relational:IsForeignKeyExcludedFromMigrations", true); return runtimeForeignKey; } diff --git a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/No_NativeAOT/DependentBaseEntityType.cs b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/No_NativeAOT/DependentBaseEntityType.cs index e400fc440a2..5f4687b055c 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/No_NativeAOT/DependentBaseEntityType.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/No_NativeAOT/DependentBaseEntityType.cs @@ -104,7 +104,6 @@ public static RuntimeForeignKey CreateForeignKey2(RuntimeEntityType declaringEnt eagerLoaded: true, lazyLoadingEnabled: false); - runtimeForeignKey.AddAnnotation("Relational:IsForeignKeyExcludedFromMigrations", true); return runtimeForeignKey; }