Skip to content

Commit

Permalink
Attribute for configuring DeleteBehavior
Browse files Browse the repository at this point in the history
Fixes #9621

Added:
DeleteBehaviorAttribute - Stores DeleteBehavior set on the property.
DeleteBehaviorAttributeConvention - Triggers on ForeignKeyAdded, checks if its navigation has DeleteBehavior. If so then sets DeleteBehavior of this foreign key.

Moved:
DeleteBehavior => EFCore.Abstractions and added TypeForward in EFCore
  • Loading branch information
Vekz committed May 18, 2022
1 parent 5a759d5 commit 0a125ab
Show file tree
Hide file tree
Showing 11 changed files with 625 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ namespace Microsoft.EntityFrameworkCore;
/// <remarks>
/// <para>
/// Behaviors in the database are dependent on the database schema being created
/// appropriately. Using Entity Framework Migrations or <see cref="DatabaseFacade.EnsureCreated" />
/// will create the appropriate schema.
/// appropriately. Using Entity Framework Migrations or
/// <c>EnsureCreated()</c> will create the appropriate schema.
/// </para>
/// <para>
/// Note that the in-memory behavior for entities that are currently tracked by
/// the <see cref="DbContext" /> can be different from the behavior that happens in the database.
/// the context can be different from the behavior that happens in the database.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-cascading">Cascade delete and deleting orphans in EF Core</see> for more information and
Expand All @@ -25,16 +25,17 @@ namespace Microsoft.EntityFrameworkCore;
public enum DeleteBehavior
{
/// <summary>
/// For entities being tracked by the <see cref="DbContext" />, the values of foreign key properties in
/// For entities being tracked by the context, the values of foreign key properties in
/// dependent entities are set to null when the related principal is deleted.
/// This helps keep the graph of entities in a consistent state while they are being tracked, such that a
/// fully consistent graph can then be written to the database. If a property cannot be set to null because
/// it is not a nullable type, then an exception will be thrown when <see cref="DbContext.SaveChanges()" /> is called.
/// it is not a nullable type, then an exception will be thrown when
/// <c>SaveChanges()</c> is called.
/// </summary>
/// <remarks>
/// <para>
/// If the database has been created from the model using Entity Framework Migrations or the
/// <see cref="DatabaseFacade.EnsureCreated" /> method, then the behavior in the database
/// <c>EnsureCreated()</c> method, then the behavior in the database
/// is to generate an error if a foreign key constraint is violated.
/// </para>
/// <para>
Expand All @@ -45,29 +46,30 @@ public enum DeleteBehavior
ClientSetNull,

/// <summary>
/// For entities being tracked by the <see cref="DbContext" />, the values of foreign key properties in
/// dependent entities are set to null when the related principal is deleted.
/// For entities being tracked by the context, the values of foreign key properties in dependent entities
/// are set to null when the related principal is deleted.
/// This helps keep the graph of entities in a consistent state while they are being tracked, such that a
/// fully consistent graph can then be written to the database. If a property cannot be set to null because
/// it is not a nullable type, then an exception will be thrown when <see cref="DbContext.SaveChanges()" /> is called.
/// it is not a nullable type, then an exception will be thrown when
/// <c>SaveChanges()</c> is called.
/// </summary>
/// <remarks>
/// If the database has been created from the model using Entity Framework Migrations or the
/// <see cref="DatabaseFacade.EnsureCreated" /> method, then the behavior in the database
/// is to generate an error if a foreign key constraint is violated.
/// <c>EnsureCreated()</c> method, then the behavior in the database is to generate an error if a foreign key constraint is violated.
/// </remarks>
Restrict,

/// <summary>
/// For entities being tracked by the <see cref="DbContext" />, the values of foreign key properties in
/// For entities being tracked by the context, the values of foreign key properties in
/// dependent entities are set to null when the related principal is deleted.
/// This helps keep the graph of entities in a consistent state while they are being tracked, such that a
/// fully consistent graph can then be written to the database. If a property cannot be set to null because
/// it is not a nullable type, then an exception will be thrown when <see cref="DbContext.SaveChanges()" /> is called.
/// it is not a nullable type, then an exception will be thrown when
/// <c>SaveChanges()</c> is called.
/// </summary>
/// <remarks>
/// If the database has been created from the model using Entity Framework Migrations or the
/// <see cref="DatabaseFacade.EnsureCreated" /> method, then the behavior in the database is
/// <c>EnsureCreated()</c> method, then the behavior in the database is
/// the same as is described above for tracked entities. Keep in mind that some databases cannot easily
/// support this behavior, especially if there are cycles in relationships, in which case it may
/// be better to use <see cref="ClientSetNull" /> which will allow EF to cascade null values
Expand All @@ -76,13 +78,13 @@ public enum DeleteBehavior
SetNull,

/// <summary>
/// For entities being tracked by the <see cref="DbContext" />, dependent entities
/// For entities being tracked by the context, dependent entities
/// will be deleted when the related principal is deleted.
/// </summary>
/// <remarks>
/// <para>
/// If the database has been created from the model using Entity Framework Migrations or the
/// <see cref="DatabaseFacade.EnsureCreated" /> method, then the behavior in the database is
/// <c>EnsureCreated()</c> method, then the behavior in the database is
/// the same as is described above for tracked entities. Keep in mind that some databases cannot easily
/// support this behavior, especially if there are cycles in relationships, in which case it may
/// be better to use <see cref="ClientCascade" /> which will allow EF to perform cascade deletes
Expand All @@ -96,27 +98,25 @@ public enum DeleteBehavior
Cascade,

/// <summary>
/// For entities being tracked by the <see cref="DbContext" />, dependent entities
/// For entities being tracked by the context, dependent entities
/// will be deleted when the related principal is deleted.
/// </summary>
/// <remarks>
/// If the database has been created from the model using Entity Framework Migrations or the
/// <see cref="DatabaseFacade.EnsureCreated" /> method, then the behavior in the database
/// is to generate an error if a foreign key constraint is violated.
/// <c>EnsureCreated()</c> method, then the behavior in the database is to generate an error if a foreign key constraint is violated.
/// </remarks>
ClientCascade,

/// <summary>
/// For entities being tracked by the <see cref="DbContext" />, the values of foreign key properties in
/// dependent entities are set to null when the related principal is deleted.
/// For entities being tracked by the context, the values of foreign key properties in dependent entities are set to null when the related principal is deleted.
/// This helps keep the graph of entities in a consistent state while they are being tracked, such that a
/// fully consistent graph can then be written to the database. If a property cannot be set to null because
/// it is not a nullable type, then an exception will be thrown when <see cref="DbContext.SaveChanges()" /> is called.
/// it is not a nullable type, then an exception will be thrown when
/// <c>SaveChanges()</c> is called.
/// </summary>
/// <remarks>
/// If the database has been created from the model using Entity Framework Migrations or the
/// <see cref="DatabaseFacade.EnsureCreated" /> method, then the behavior in the database
/// is to generate an error if a foreign key constraint is violated.
/// <c>EnsureCreated()</c> method, then the behavior in the database is to generate an error if a foreign key constraint is violated.
/// </remarks>
NoAction,

Expand All @@ -126,15 +126,13 @@ public enum DeleteBehavior
/// </summary>
/// <remarks>
/// <para>
/// For entities being tracked by the <see cref="DbContext" />, the values of foreign key properties in
/// dependent entities are not changed when the related principal entity is deleted.
/// For entities being tracked by the context, the values of foreign key properties in dependent entities are not changed when the related principal entity is deleted.
/// This can result in an inconsistent graph of entities where the values of foreign key properties do
/// not match the relationships in the graph.
/// </para>
/// <para>
/// If the database has been created from the model using Entity Framework Migrations or the
/// <see cref="DatabaseFacade.EnsureCreated" /> method, then the behavior in the database
/// is to generate an error if a foreign key constraint is violated.
/// <c>EnsureCreated()</c> method, then the behavior in the database is to generate an error if a foreign key constraint is violated.
/// </para>
/// </remarks>
ClientNoAction
Expand Down
30 changes: 30 additions & 0 deletions src/EFCore.Abstractions/DeleteBehaviorAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore;

/// <summary>
/// Configures the navigation property on the dependent side of a relationship
/// to indicate how a delete operation is applied to dependent entities
/// in a relationship when it is deleted or the relationship is severed.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-modeling">Modeling entity types and relationships</see> for more information and examples.
/// </remarks>
[AttributeUsage(AttributeTargets.Property)]
public sealed class DeleteBehaviorAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="DeleteBehaviorAttribute" /> class.
/// </summary>
/// <param name="behavior">The <see cref="DeleteBehavior" /> to be configured.</param>
public DeleteBehaviorAttribute(DeleteBehavior behavior)
{
this.Behavior = behavior;
}

/// <summary>
/// Gets the <see cref="DeleteBehavior" /> to be configured.
/// </summary>
public DeleteBehavior Behavior { get; }
}
111 changes: 111 additions & 0 deletions src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore.Metadata.Conventions;

using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure;

/// <summary>
/// A convention that configures the delete behavior based on the <see cref="DeleteBehaviorAttribute" /> applied on the property.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-conventions">Model building conventions</see> for more information and examples.
/// </remarks>
public class DeleteBehaviorAttributeConvention : PropertyAttributeConventionBase<DeleteBehaviorAttribute>, INavigationAddedConvention, IForeignKeyPrincipalEndChangedConvention, IModelFinalizingConvention
{
/// <summary>
/// Initializes a new instance of the <see cref="DeleteBehaviorAttributeConvention"/> class.
/// </summary>
/// <param name="dependencies">Parameter object containing dependencies for this convention.</param>
public DeleteBehaviorAttributeConvention(ProviderConventionSetBuilderDependencies dependencies)
: base(dependencies)
{
}

/// <summary>
/// Called after a navigation is added to the entity type.
/// </summary>
/// <param name="navigationBuilder">The builder for the navigation.</param>
/// <param name="context">Additional information associated with convention execution.</param>
public virtual void ProcessNavigationAdded(IConventionNavigationBuilder navigationBuilder, IConventionContext<IConventionNavigationBuilder> context)
{
var navAttribute = navigationBuilder.Metadata.PropertyInfo?.GetCustomAttribute<DeleteBehaviorAttribute>();
if (navAttribute == null)
{
return;
}

var foreignKey = navigationBuilder.Metadata.ForeignKey;
if (!navigationBuilder.Metadata.IsOnDependent && foreignKey.IsUnique)
{
return;
}

foreignKey.Builder.OnDelete(navAttribute.Behavior, fromDataAnnotation: true);
}

/// <summary>
/// Called after the principal end of a foreign key is changed.
/// </summary>
/// <param name="relationshipBuilder">The builder for the foreign key.</param>
/// <param name="context">Additional information associated with convention execution.</param>
public virtual void ProcessForeignKeyPrincipalEndChanged(IConventionForeignKeyBuilder relationshipBuilder, IConventionContext<IConventionForeignKeyBuilder> context)
{
if (!relationshipBuilder.Metadata.IsUnique)
{
return;
}

var navigation = relationshipBuilder.Metadata.DependentToPrincipal;
var navAttribute = navigation?.PropertyInfo?.GetCustomAttribute<DeleteBehaviorAttribute>();
if (navAttribute == null)
{
return;
}

relationshipBuilder.OnDelete(navAttribute.Behavior, fromDataAnnotation: true);
}

/// <summary>
/// Called when a model is being finalized.
/// </summary>
/// <param name="modelBuilder">The builder for the model.</param>
/// <param name="context">Additional information associated with convention execution.</param>
public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
{
foreach (var entityType in modelBuilder.Metadata.GetEntityTypes())
{
foreach (var navigation in entityType.GetNavigations())
{
var navAttribute = navigation.PropertyInfo?.GetCustomAttribute<DeleteBehaviorAttribute>();
if (navAttribute == null)
{
return;
}

if (!navigation.IsOnDependent)
{
throw new InvalidOperationException(CoreStrings.DeleteBehaviorAttributeOnPrincipalProperty);
}
}
}
}

/// <summary>
/// Called after a property is added to the entity type with an attribute on the associated CLR property or field.
/// </summary>
/// <param name="propertyBuilder">The builder for the property.</param>
/// <param name="attribute">The attribute.</param>
/// <param name="clrMember">The member that has the attribute.</param>
/// <param name="context">Additional information associated with convention execution.</param>
protected override void ProcessPropertyAdded(
IConventionPropertyBuilder propertyBuilder,
DeleteBehaviorAttribute attribute,
MemberInfo clrMember,
IConventionContext context)
{
throw new InvalidOperationException(CoreStrings.DeleteBehaviorAttributeNotOnNavigationProperty);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ public virtual ConventionSet CreateConventionSet()
var backingFieldAttributeConvention = new BackingFieldAttributeConvention(Dependencies);
var unicodeAttributeConvention = new UnicodeAttributeConvention(Dependencies);
var precisionAttributeConvention = new PrecisionAttributeConvention(Dependencies);
var deleteBehaviorAttributeConvention = new DeleteBehaviorAttributeConvention(Dependencies);

conventionSet.PropertyAddedConventions.Add(backingFieldAttributeConvention);
conventionSet.PropertyAddedConventions.Add(backingFieldConvention);
Expand All @@ -131,6 +132,7 @@ public virtual ConventionSet CreateConventionSet()
conventionSet.PropertyAddedConventions.Add(foreignKeyPropertyDiscoveryConvention);
conventionSet.PropertyAddedConventions.Add(unicodeAttributeConvention);
conventionSet.PropertyAddedConventions.Add(precisionAttributeConvention);
conventionSet.PropertyAddedConventions.Add(deleteBehaviorAttributeConvention);

conventionSet.EntityTypePrimaryKeyChangedConventions.Add(foreignKeyPropertyDiscoveryConvention);
conventionSet.EntityTypePrimaryKeyChangedConventions.Add(valueGeneratorConvention);
Expand Down Expand Up @@ -186,6 +188,7 @@ public virtual ConventionSet CreateConventionSet()
conventionSet.NavigationAddedConventions.Add(foreignKeyPropertyDiscoveryConvention);
conventionSet.NavigationAddedConventions.Add(relationshipDiscoveryConvention);
conventionSet.NavigationAddedConventions.Add(foreignKeyAttributeConvention);
conventionSet.NavigationAddedConventions.Add(deleteBehaviorAttributeConvention);

var manyToManyJoinEntityTypeConvention = new ManyToManyJoinEntityTypeConvention(Dependencies);
conventionSet.SkipNavigationAddedConventions.Add(new NavigationBackingFieldAttributeConvention(Dependencies));
Expand Down Expand Up @@ -213,6 +216,7 @@ public virtual ConventionSet CreateConventionSet()
conventionSet.ForeignKeyPrincipalEndChangedConventions.Add(foreignKeyPropertyDiscoveryConvention);
conventionSet.ForeignKeyPrincipalEndChangedConventions.Add(requiredNavigationAttributeConvention);
conventionSet.ForeignKeyPrincipalEndChangedConventions.Add(nonNullableNavigationConvention);
conventionSet.ForeignKeyPrincipalEndChangedConventions.Add(deleteBehaviorAttributeConvention);

conventionSet.PropertyNullabilityChangedConventions.Add(foreignKeyPropertyDiscoveryConvention);

Expand Down Expand Up @@ -241,6 +245,7 @@ public virtual ConventionSet CreateConventionSet()
conventionSet.ModelFinalizingConventions.Add(new QueryFilterRewritingConvention(Dependencies));
conventionSet.ModelFinalizingConventions.Add(inversePropertyAttributeConvention);
conventionSet.ModelFinalizingConventions.Add(backingFieldConvention);
conventionSet.ModelFinalizingConventions.Add(deleteBehaviorAttributeConvention);

conventionSet.ModelFinalizedConventions.Add(new RuntimeModelConvention(Dependencies));

Expand Down
Loading

0 comments on commit 0a125ab

Please sign in to comment.