Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid cascade cycles on SQL Server for derived-type referencing many-to-many #28937

Merged
merged 3 commits into from
Sep 2, 2022
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions;
/// <see href="https://aka.ms/efcore-docs-sqlserver">Accessing SQL Server and SQL Azure databases with EF Core</see>
/// for more information and examples.
/// </remarks>
public class SqlServerOnDeleteConvention : CascadeDeleteConvention, ISkipNavigationForeignKeyChangedConvention
public class SqlServerOnDeleteConvention : CascadeDeleteConvention,
ISkipNavigationForeignKeyChangedConvention,
IEntityTypeAnnotationChangedConvention
{
/// <summary>
/// Creates a new instance of <see cref="SqlServerOnDeleteConvention" />.
Expand Down Expand Up @@ -60,23 +62,77 @@ protected override DeleteBehavior GetTargetDeleteBehavior(IConventionForeignKey
return DeleteBehavior.ClientCascade;
}

var selfReferencingSkipNavigation = foreignKey.GetReferencingSkipNavigations()
.FirstOrDefault(s => s.Inverse != null && s.TargetEntityType == s.DeclaringEntityType);
if (selfReferencingSkipNavigation == null)
return ProcessSkipNavigations(foreignKey.GetReferencingSkipNavigations()) ?? deleteBehavior;
}

private DeleteBehavior? ProcessSkipNavigations(IEnumerable<IConventionSkipNavigation> skipNavigations)
{
var skipNavigation = skipNavigations
.FirstOrDefault(
s => s.Inverse != null
&& IsMappedToSameTable(s.DeclaringEntityType, s.TargetEntityType));

if (skipNavigation != null)
{
return deleteBehavior;
var isFirstSkipNavigation = IsFirstSkipNavigation(skipNavigation);
if (!isFirstSkipNavigation)
{
skipNavigation = skipNavigation.Inverse!;
}

var inverseSkipNavigation = skipNavigation.Inverse!;

var deleteBehavior = DefaultDeleteBehavior(skipNavigation);
var inverseDeleteBehavior = DefaultDeleteBehavior(inverseSkipNavigation);
Copy link
Member

Choose a reason for hiding this comment

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

Probably a corner case, but the inverse nav could already be configured as ClientCascade explicitly, so we wouldn't need to do anything


if (deleteBehavior == DeleteBehavior.Cascade
&& inverseDeleteBehavior == DeleteBehavior.Cascade
&& !(inverseSkipNavigation.ForeignKey!.GetDeleteBehaviorConfigurationSource() == ConfigurationSource.Explicit
&& inverseSkipNavigation.ForeignKey!.DeleteBehavior != DeleteBehavior.Cascade))
{
deleteBehavior = DeleteBehavior.ClientCascade;
}

skipNavigation.ForeignKey!.Builder.OnDelete(deleteBehavior);
inverseSkipNavigation.ForeignKey!.Builder.OnDelete(inverseDeleteBehavior);

return isFirstSkipNavigation ? deleteBehavior : inverseDeleteBehavior;
}

if (selfReferencingSkipNavigation
== selfReferencingSkipNavigation.DeclaringEntityType.GetDeclaredSkipNavigations()
.First(s => s == selfReferencingSkipNavigation || s == selfReferencingSkipNavigation.Inverse)
&& selfReferencingSkipNavigation != selfReferencingSkipNavigation.Inverse)
return null;

DeleteBehavior DefaultDeleteBehavior(IConventionSkipNavigation conventionSkipNavigation)
=> conventionSkipNavigation.ForeignKey!.IsRequired ? DeleteBehavior.Cascade : DeleteBehavior.ClientSetNull;

bool IsMappedToSameTable(IConventionEntityType entityType1, IConventionEntityType entityType2)
{
selfReferencingSkipNavigation.Inverse!.ForeignKey?.Builder.OnDelete(
GetTargetDeleteBehavior(selfReferencingSkipNavigation.Inverse.ForeignKey));
return DeleteBehavior.ClientCascade;
var tableName1 = entityType1.GetTableName();
var tableName2 = entityType2.GetTableName();

return tableName1 != null
&& tableName2 != null
&& tableName1 == tableName2
&& entityType1.GetSchema() == entityType2.GetSchema();
}

return deleteBehavior;
bool IsFirstSkipNavigation(IConventionSkipNavigation navigation)
=> navigation.DeclaringEntityType != navigation.TargetEntityType
? string.Compare(navigation.DeclaringEntityType.Name, navigation.TargetEntityType.Name, StringComparison.Ordinal) < 0
: string.Compare(navigation.Name, navigation.Inverse!.Name, StringComparison.Ordinal) < 0;
}

/// <inheritdoc />
public virtual void ProcessEntityTypeAnnotationChanged(
IConventionEntityTypeBuilder entityTypeBuilder,
string name,
IConventionAnnotation? annotation,
IConventionAnnotation? oldAnnotation,
IConventionContext<IConventionAnnotation> context)
{
if (name == RelationalAnnotationNames.TableName
|| name == RelationalAnnotationNames.Schema)
{
ProcessSkipNavigations(entityTypeBuilder.Metadata.GetDeclaredSkipNavigations());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1566,7 +1566,7 @@ public static RuntimeForeignKey CreateForeignKey2(RuntimeEntityType declaringEnt
var runtimeForeignKey = declaringEntityType.AddForeignKey(new[] { declaringEntityType.FindProperty(""PrincipalsId"")!, declaringEntityType.FindProperty(""PrincipalsAlternateId"")! },
principalEntityType.FindKey(new[] { principalEntityType.FindProperty(""Id"")!, principalEntityType.FindProperty(""AlternateId"")! })!,
principalEntityType,
deleteBehavior: DeleteBehavior.Cascade,
deleteBehavior: DeleteBehavior.ClientCascade,
required: true);

return runtimeForeignKey;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.TestModels.ManyToManyModel;

namespace Microsoft.EntityFrameworkCore;

public abstract class ManyToManyTrackingRelationalTestBase<TFixture> : ManyToManyTrackingTestBase<TFixture>
where TFixture : ManyToManyTrackingRelationalTestBase<TFixture>.ManyToManyTrackingRelationalFixture
{
protected ManyToManyTrackingRelationalTestBase(TFixture fixture)
: base(fixture)
{
}

[ConditionalFact]
public void Many_to_many_delete_behaviors_are_set()
{
using var context = CreateContext();
var model = context.Model;

var navigations = model.GetEntityTypes().SelectMany(e => e.GetDeclaredSkipNavigations())
.Where(e => e.ForeignKey.DeleteBehavior != DeleteBehavior.Cascade).ToList();

var builder = new StringBuilder();
foreach (var navigation in navigations)
{
builder.AppendLine($"{{ \"{navigation.DeclaringEntityType.ShortName()}.{navigation.Name}\", DeleteBehavior.ClientCascade }},");
}

var x = builder.ToString();

foreach (var skipNavigation in model.GetEntityTypes().SelectMany(e => e.GetSkipNavigations()))
{
Assert.Equal(
CustomDeleteBehaviors.TryGetValue(
$"{skipNavigation.DeclaringEntityType.ShortName()}.{skipNavigation.Name}", out var deleteBehavior)
? deleteBehavior
: DeleteBehavior.Cascade,
skipNavigation.ForeignKey.DeleteBehavior);
}
}

protected virtual Dictionary<string, DeleteBehavior> CustomDeleteBehaviors { get; } = new();

protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction)
=> facade.UseTransaction(transaction.GetDbTransaction());

public abstract class ManyToManyTrackingRelationalFixture : ManyToManyTrackingFixtureBase
{
protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
{
base.OnModelCreating(modelBuilder, context);

modelBuilder.Entity<EntityTableSharing1>().ToTable("TableSharing");
modelBuilder.Entity<EntityTableSharing2>(
b =>
{
b.HasOne<EntityTableSharing1>().WithOne().HasForeignKey<EntityTableSharing2>(e => e.Id);
b.ToTable("TableSharing");
});
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.TestModels.ManyToManyModel;

namespace Microsoft.EntityFrameworkCore.Query;

public abstract class ManyToManyQueryRelationalFixture : ManyToManyQueryFixtureBase
{
public TestSqlLoggerFactory TestSqlLoggerFactory
=> (TestSqlLoggerFactory)ListLoggerFactory;

protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
{
base.OnModelCreating(modelBuilder, context);

modelBuilder.Entity<EntityTableSharing1>().ToTable("TableSharing");
modelBuilder.Entity<EntityTableSharing2>(
b =>
{
b.HasOne<EntityTableSharing1>().WithOne().HasForeignKey<EntityTableSharing2>(e => e.Id);
b.ToTable("TableSharing");
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
modelBuilder.Entity<EntityRoot>().ToTable("Roots");
modelBuilder.Entity<EntityBranch>().ToTable("Branches");
modelBuilder.Entity<EntityLeaf>().ToTable("Leaves");
modelBuilder.Entity<EntityBranch2>().ToTable("Branch2s");
modelBuilder.Entity<EntityLeaf2>().ToTable("Leaf2s");

modelBuilder.Entity<UnidirectionalEntityRoot>().UseTpcMappingStrategy();
modelBuilder.Entity<UnidirectionalEntityRoot>().ToTable("UnidirectionalRoots");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
modelBuilder.Entity<EntityRoot>().ToTable("Roots");
modelBuilder.Entity<EntityBranch>().ToTable("Branches");
modelBuilder.Entity<EntityLeaf>().ToTable("Leaves");
modelBuilder.Entity<EntityBranch2>().ToTable("Branch2s");
modelBuilder.Entity<EntityLeaf2>().ToTable("Leaf2s");

modelBuilder.Entity<UnidirectionalEntityRoot>().ToTable("UnidirectionalRoots");
modelBuilder.Entity<UnidirectionalEntityBranch>().ToTable("UnidirectionalBranches");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
.HasMany(e => e.TwoSkipShared)
.WithMany(e => e.OneSkipShared);

modelBuilder.Entity<EntityRoot>()
.HasMany(e => e.BranchSkipShared)
.WithMany(e => e.RootSkipShared);

// Nav:2 Payload:No Join:Concrete Extra:None
modelBuilder.Entity<EntityOne>()
.HasMany(e => e.TwoSkip)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ public ISetSource GetExpectedData()
{ typeof(EntityRoot), e => ((EntityRoot)e)?.Id },
{ typeof(EntityBranch), e => ((EntityBranch)e)?.Id },
{ typeof(EntityLeaf), e => ((EntityLeaf)e)?.Id },
{ typeof(EntityBranch2), e => ((EntityBranch2)e)?.Id },
{ typeof(EntityLeaf2), e => ((EntityLeaf2)e)?.Id },
{ typeof(EntityTableSharing1), e => ((EntityTableSharing1)e)?.Id },
{ typeof(EntityTableSharing2), e => ((EntityTableSharing2)e)?.Id },
{ typeof(UnidirectionalEntityOne), e => ((UnidirectionalEntityOne)e)?.Id },
{ typeof(UnidirectionalEntityTwo), e => ((UnidirectionalEntityTwo)e)?.Id },
{ typeof(UnidirectionalEntityThree), e => ((UnidirectionalEntityThree)e)?.Id },
Expand Down Expand Up @@ -165,6 +169,69 @@ public ISetSource GetExpectedData()
}
}
},
{
typeof(EntityBranch2), (e, a) =>
{
Assert.Equal(e == null, a == null);

if (a != null)
{
var ee = (EntityBranch2)e;
var aa = (EntityBranch2)a;

Assert.Equal(ee.Id, aa.Id);
Assert.Equal(ee.Name, aa.Name);
Assert.Equal(ee.Slumber, aa.Slumber);
}
}
},
{
typeof(EntityLeaf2), (e, a) =>
{
Assert.Equal(e == null, a == null);

if (a != null)
{
var ee = (EntityLeaf2)e;
var aa = (EntityLeaf2)a;

Assert.Equal(ee.Id, aa.Id);
Assert.Equal(ee.Name, aa.Name);
Assert.Equal(ee.Slumber, aa.Slumber);
Assert.Equal(ee.IsBrown, aa.IsBrown);
}
}
},
{
typeof(EntityTableSharing1), (e, a) =>
{
Assert.Equal(e == null, a == null);

if (a != null)
{
var ee = (EntityTableSharing1)e;
var aa = (EntityTableSharing1)a;

Assert.Equal(ee.Id, aa.Id);
Assert.Equal(ee.Name, aa.Name);
}
}
},
{
typeof(EntityTableSharing2), (e, a) =>
{
Assert.Equal(e == null, a == null);

if (a != null)
{
var ee = (EntityTableSharing2)e;
var aa = (EntityTableSharing2)a;

Assert.Equal(ee.Id, aa.Id);
Assert.Equal(ee.Cucumber, aa.Cucumber);
}
}
},
{
typeof(UnidirectionalEntityOne), (e, a) =>
{
Expand Down Expand Up @@ -292,6 +359,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
modelBuilder.Entity<EntityRoot>().Property(e => e.Id).ValueGeneratedNever();
modelBuilder.Entity<EntityBranch>().HasBaseType<EntityRoot>();
modelBuilder.Entity<EntityLeaf>().HasBaseType<EntityBranch>();
modelBuilder.Entity<EntityBranch2>().HasBaseType<EntityRoot>();
modelBuilder.Entity<EntityLeaf2>().HasBaseType<EntityBranch2>();
modelBuilder.Entity<EntityTableSharing1>().Property(e => e.Id).ValueGeneratedNever();
modelBuilder.Entity<EntityTableSharing2>().Property(e => e.Id).ValueGeneratedNever();

modelBuilder.Entity<UnidirectionalEntityOne>().Property(e => e.Id).ValueGeneratedNever();
modelBuilder.Entity<UnidirectionalEntityTwo>().Property(e => e.Id).ValueGeneratedNever();
Expand Down Expand Up @@ -321,6 +392,22 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
.HasMany(e => e.TwoSkipShared)
.WithMany(e => e.OneSkipShared);

modelBuilder.Entity<EntityRoot>()
.HasMany(e => e.BranchSkipShared)
.WithMany(e => e.RootSkipShared);

modelBuilder.Entity<EntityBranch2>()
.HasMany(e => e.SelfSkipSharedLeft)
.WithMany(e => e.SelfSkipSharedRight);

modelBuilder.Entity<EntityBranch2>()
.HasMany(e => e.Leaf2SkipShared)
.WithMany(e => e.Branch2SkipShared);

modelBuilder.Entity<EntityTableSharing1>()
.HasMany(e => e.TableSharing2Shared)
.WithMany(e => e.TableSharing1Shared);

// Nav:2 Payload:No Join:Concrete Extra:None
modelBuilder.Entity<EntityOne>()
.HasMany(e => e.TwoSkip)
Expand Down Expand Up @@ -439,6 +526,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
.HasMany(e => e.TwoSkipShared)
.WithMany();

modelBuilder.Entity<UnidirectionalEntityBranch>()
.HasMany<UnidirectionalEntityRoot>()
.WithMany(e => e.BranchSkipShared);

// Nav:2 Payload:No Join:Concrete Extra:None
modelBuilder.Entity<UnidirectionalEntityOne>()
.HasMany(e => e.TwoSkip)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ public class EntityBranch : EntityRoot
{
public long Number;
public ICollection<EntityOne> OneSkip;
public ICollection<EntityRoot> RootSkipShared;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ public class EntityRoot
public string Name;
public ICollection<EntityThree> ThreeSkipShared;
public ICollection<EntityCompositeKey> CompositeKeySkipShared;
public ICollection<EntityBranch> BranchSkipShared;
}
Loading