Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/EFCore.Relational/Design/AnnotationCodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,12 @@ public virtual IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(
nameof(RelationalForeignKeyBuilderExtensions.HasConstraintName),
methodCallCodeFragments);

GenerateSimpleFluentApiCall(
annotations,
RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations,
nameof(RelationalForeignKeyBuilderExtensions.ExcludeFromMigrations),
methodCallCodeFragments);
Comment on lines +438 to +442
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the new IsForeignKeyExcludedFromMigrations fluent API call generation, if the annotation ever ends up explicitly set to false (e.g. toggled from true back to false), the snapshot will likely emit .ExcludeFromMigrations(false) since RemoveAnnotationsHandledByConventions(IForeignKey, …) doesn’t special-case this annotation. Consider removing this annotation when its value is false (similar to how IsTableExcludedFromMigrations is handled) to avoid noisy/unstable snapshots.

Suggested change
GenerateSimpleFluentApiCall(
annotations,
RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations,
nameof(RelationalForeignKeyBuilderExtensions.ExcludeFromMigrations),
methodCallCodeFragments);
if (annotations.TryGetValue(
RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations,
out var excludeFromMigrationsAnnotation)
&& excludeFromMigrationsAnnotation.Value is bool isExcludedFromMigrations
&& !isExcludedFromMigrations)
{
annotations.Remove(RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations);
}
else
{
GenerateSimpleFluentApiCall(
annotations,
RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations,
nameof(RelationalForeignKeyBuilderExtensions.ExcludeFromMigrations),
methodCallCodeFragments);
}

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AndriySvyryd don't we actually prefer to bake in an explicit false when it's explicitly configured by the user?


methodCallCodeFragments.AddRange(GenerateFluentApiCallsHelper(foreignKey, annotations, GenerateFluentApi));

return methodCallCodeFragments;
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,4 +173,166 @@ public static bool CanSetConstraintName(
string? name,
bool fromDataAnnotation = false)
=> relationship.CanSetAnnotation(RelationalAnnotationNames.Name, name, fromDataAnnotation);

/// <summary>
/// Configures whether the foreign key constraint is excluded from migrations
/// when targeting a relational database.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-modeling">Modeling entity types and relationships</see> for more information and examples.
/// </remarks>
/// <param name="referenceCollectionBuilder">The builder being used to configure the relationship.</param>
/// <param name="excluded">A value indicating whether the foreign key constraint is excluded from migrations.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
public static ReferenceCollectionBuilder ExcludeFromMigrations(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AndriySvyryd see suggestion to rename this to ExcludeForeignKeyFromMigrations, to make it clear what's being excluded.

this ReferenceCollectionBuilder referenceCollectionBuilder,
bool excluded = true)
{
referenceCollectionBuilder.Metadata.SetIsExcludedFromMigrations(excluded);

return referenceCollectionBuilder;
}

/// <summary>
/// Configures whether the foreign key constraint is excluded from migrations
/// when targeting a relational database.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-modeling">Modeling entity types and relationships</see> for more information and examples.
/// </remarks>
/// <param name="referenceCollectionBuilder">The builder being used to configure the relationship.</param>
/// <param name="excluded">A value indicating whether the foreign key constraint is excluded from migrations.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
/// <typeparam name="TEntity">The principal entity type in this relationship.</typeparam>
/// <typeparam name="TRelatedEntity">The dependent entity type in this relationship.</typeparam>
public static ReferenceCollectionBuilder<TEntity, TRelatedEntity> ExcludeFromMigrations<TEntity, TRelatedEntity>(
this ReferenceCollectionBuilder<TEntity, TRelatedEntity> referenceCollectionBuilder,
bool excluded = true)
where TEntity : class
where TRelatedEntity : class
=> (ReferenceCollectionBuilder<TEntity, TRelatedEntity>)ExcludeFromMigrations(
(ReferenceCollectionBuilder)referenceCollectionBuilder, excluded);

/// <summary>
/// Configures whether the foreign key constraint is excluded from migrations
/// when targeting a relational database.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-modeling">Modeling entity types and relationships</see> for more information and examples.
/// </remarks>
/// <param name="referenceReferenceBuilder">The builder being used to configure the relationship.</param>
/// <param name="excluded">A value indicating whether the foreign key constraint is excluded from migrations.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
public static ReferenceReferenceBuilder ExcludeFromMigrations(
this ReferenceReferenceBuilder referenceReferenceBuilder,
bool excluded = true)
{
referenceReferenceBuilder.Metadata.SetIsExcludedFromMigrations(excluded);

return referenceReferenceBuilder;
}

/// <summary>
/// Configures whether the foreign key constraint is excluded from migrations
/// when targeting a relational database.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-modeling">Modeling entity types and relationships</see> for more information and examples.
/// </remarks>
/// <param name="referenceReferenceBuilder">The builder being used to configure the relationship.</param>
/// <param name="excluded">A value indicating whether the foreign key constraint is excluded from migrations.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
/// <typeparam name="TEntity">The entity type on one end of the relationship.</typeparam>
/// <typeparam name="TRelatedEntity">The entity type on the other end of the relationship.</typeparam>
public static ReferenceReferenceBuilder<TEntity, TRelatedEntity> ExcludeFromMigrations<TEntity, TRelatedEntity>(
this ReferenceReferenceBuilder<TEntity, TRelatedEntity> referenceReferenceBuilder,
bool excluded = true)
where TEntity : class
where TRelatedEntity : class
=> (ReferenceReferenceBuilder<TEntity, TRelatedEntity>)ExcludeFromMigrations(
(ReferenceReferenceBuilder)referenceReferenceBuilder, excluded);

/// <summary>
/// Configures whether the foreign key constraint is excluded from migrations
/// when targeting a relational database.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-modeling">Modeling entity types and relationships</see> for more information and examples.
/// </remarks>
/// <param name="ownershipBuilder">The builder being used to configure the relationship.</param>
/// <param name="excluded">A value indicating whether the foreign key constraint is excluded from migrations.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
public static OwnershipBuilder ExcludeFromMigrations(
this OwnershipBuilder ownershipBuilder,
bool excluded = true)
{
ownershipBuilder.Metadata.SetIsExcludedFromMigrations(excluded);

return ownershipBuilder;
}

/// <summary>
/// Configures whether the foreign key constraint is excluded from migrations
/// when targeting a relational database.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-modeling">Modeling entity types and relationships</see> for more information and examples.
/// </remarks>
/// <param name="ownershipBuilder">The builder being used to configure the relationship.</param>
/// <param name="excluded">A value indicating whether the foreign key constraint is excluded from migrations.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
/// <typeparam name="TEntity">The entity type on one end of the relationship.</typeparam>
/// <typeparam name="TDependentEntity">The entity type on the other end of the relationship.</typeparam>
public static OwnershipBuilder<TEntity, TDependentEntity> ExcludeFromMigrations<TEntity, TDependentEntity>(
this OwnershipBuilder<TEntity, TDependentEntity> ownershipBuilder,
bool excluded = true)
where TEntity : class
where TDependentEntity : class
=> (OwnershipBuilder<TEntity, TDependentEntity>)ExcludeFromMigrations(
(OwnershipBuilder)ownershipBuilder, excluded);

/// <summary>
/// Configures whether the foreign key constraint is excluded from migrations
/// when targeting a relational database.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-modeling">Modeling entity types and relationships</see> for more information and examples.
/// </remarks>
/// <param name="relationship">The builder being used to configure the relationship.</param>
/// <param name="excluded">A value indicating whether the foreign key constraint is excluded from migrations.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>
/// The same builder instance if the configuration was applied,
/// <see langword="null" /> otherwise.
/// </returns>
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;
}

/// <summary>
/// Returns a value indicating whether the foreign key constraint exclusion from migrations can be set
/// from the current configuration source.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-modeling">Modeling entity types and relationships</see> for more information and examples.
/// </remarks>
/// <param name="relationship">The builder being used to configure the relationship.</param>
/// <param name="excluded">A value indicating whether the foreign key constraint is excluded from migrations.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns><see langword="true" /> if the configuration can be applied.</returns>
public static bool CanSetExcludeFromMigrations(
this IConventionForeignKeyBuilder relationship,
bool? excluded,
bool fromDataAnnotation = false)
=> relationship.CanSetAnnotation(RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations, excluded, fromDataAnnotation);
}
Original file line number Diff line number Diff line change
Expand Up @@ -288,4 +288,45 @@ static bool IsMapped(IReadOnlyForeignKey foreignKey, StoreObjectIdentifier store
this IForeignKey foreignKey,
in StoreObjectIdentifier storeObject)
=> (IForeignKey?)((IReadOnlyForeignKey)foreignKey).FindSharedObjectRootForeignKey(storeObject);

/// <summary>
/// Returns a value indicating whether the foreign key constraint is excluded from migrations.
/// </summary>
/// <param name="foreignKey">The foreign key.</param>
/// <returns><see langword="true" /> if the foreign key constraint is excluded from migrations.</returns>
public static bool IsExcludedFromMigrations(this IReadOnlyForeignKey foreignKey)
=> (bool?)foreignKey[RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations] ?? false;

/// <summary>
/// Sets a value indicating whether the foreign key constraint is excluded from migrations.
/// </summary>
/// <param name="foreignKey">The foreign key.</param>
/// <param name="excluded">The value to set.</param>
public static void SetIsExcludedFromMigrations(this IMutableForeignKey foreignKey, bool? excluded)
=> foreignKey.SetOrRemoveAnnotation(RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations, excluded);

/// <summary>
/// Sets a value indicating whether the foreign key constraint is excluded from migrations.
/// </summary>
/// <param name="foreignKey">The foreign key.</param>
/// <param name="excluded">The value to set.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>The configured value.</returns>
public static bool? SetIsExcludedFromMigrations(
this IConventionForeignKey foreignKey,
bool? excluded,
bool fromDataAnnotation = false)
=> (bool?)foreignKey.SetOrRemoveAnnotation(
RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations,
excluded,
fromDataAnnotation)?.Value;

/// <summary>
/// Gets the <see cref="ConfigurationSource" /> for the foreign key exclusion from migrations.
/// </summary>
/// <param name="foreignKey">The foreign key.</param>
/// <returns>The <see cref="ConfigurationSource" /> for the foreign key exclusion from migrations.</returns>
public static ConfigurationSource? GetIsExcludedFromMigrationsConfigurationSource(this IConventionForeignKey foreignKey)
=> foreignKey.FindAnnotation(RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations)
?.GetConfigurationSource();
}
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@ protected override void ProcessForeignKeyAnnotations(
if (runtime)
{
annotations.Remove(RelationalAnnotationNames.ForeignKeyMappings);
annotations.Remove(RelationalAnnotationNames.IsForeignKeyExcludedFromMigrations);
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/EFCore.Relational/Metadata/IForeignKeyConstraint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ IReadOnlyList<IColumn> PrincipalColumns
/// </summary>
ReferentialAction OnDeleteAction { get; }

/// <summary>
/// Gets a value indicating whether the foreign key constraint is excluded from migrations.
/// </summary>
bool IsExcludedFromMigrations { get; }
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding IsExcludedFromMigrations as a new abstract member on the public IForeignKeyConstraint interface is a binary breaking change for any external implementations. To preserve compatibility, consider providing a default interface implementation (e.g. computing from MappedForeignKeys or returning false) or exposing this via an extension method instead of extending the interface contract.

Suggested change
bool IsExcludedFromMigrations { get; }
bool IsExcludedFromMigrations
=> false;

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AndriySvyryd let me know what you want to do here, AFAIK these interfaces aren't meant to be implemented by external users so this is probably not relevant?


/// <summary>
/// <para>
/// Creates a human-readable representation of the given metadata.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@ public override bool IsReadOnly
/// <inheritdoc />
public virtual ReferentialAction OnDeleteAction { get; set; }

/// <summary>
/// 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.
/// </summary>
public virtual bool IsExcludedFromMigrations
=> ((IReadOnlyForeignKey)MappedForeignKeys.First()).IsExcludedFromMigrations();

/// <summary>
/// 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
6 changes: 6 additions & 0 deletions src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ public static class RelationalAnnotationNames
/// </summary>
public const string IsTableExcludedFromMigrations = Prefix + "IsTableExcludedFromMigrations";

/// <summary>
/// The name for the annotation determining whether the foreign key constraint is excluded from migrations.
/// </summary>
public const string IsForeignKeyExcludedFromMigrations = Prefix + "IsForeignKeyExcludedFromMigrations";

/// <summary>
/// The name for the annotation determining the mapping strategy for inherited properties.
/// </summary>
Expand Down Expand Up @@ -396,6 +401,7 @@ public static class RelationalAnnotationNames
IsFixedLength,
ViewDefinitionSql,
IsTableExcludedFromMigrations,
IsForeignKeyExcludedFromMigrations,
MappingStrategy,
RelationalModel,
RelationalModelFactory,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1429,7 +1429,8 @@ protected virtual IEnumerable<MigrationOperation> Diff(
protected virtual IEnumerable<MigrationOperation> Add(IForeignKeyConstraint target, DiffContext diffContext)
{
var targetTable = target.Table;
if (targetTable.IsExcludedFromMigrations)
if (targetTable.IsExcludedFromMigrations
|| target.IsExcludedFromMigrations)
{
yield break;
}
Expand All @@ -1456,7 +1457,8 @@ protected virtual IEnumerable<MigrationOperation> Add(IForeignKeyConstraint targ
protected virtual IEnumerable<MigrationOperation> Remove(IForeignKeyConstraint source, DiffContext diffContext)
{
var sourceTable = source.Table;
if (sourceTable.IsExcludedFromMigrations)
if (sourceTable.IsExcludedFromMigrations
|| source.IsExcludedFromMigrations)
{
yield break;
}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading