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

[5.0.1] Use topological sort to detect identifying relationship cycles. #23320

Merged
merged 2 commits into from
Nov 17, 2020
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
64 changes: 28 additions & 36 deletions src/EFCore/Infrastructure/ModelValidator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
Expand Down Expand Up @@ -463,50 +463,42 @@ protected virtual void ValidateNoCycles(
{
Check.NotNull(model, nameof(model));

var typesToValidate = new Queue<IEntityType>();
var reachableTypes = new HashSet<IEntityType>();
var unvalidatedEntityTypes = new HashSet<IEntityType>(model.GetEntityTypes());
while (unvalidatedEntityTypes.Count > 0)
var useOldBehavior = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue23289", out var isEnabled)
&& isEnabled;
if (useOldBehavior)
{
var rootEntityType = unvalidatedEntityTypes.First();
reachableTypes.Clear();
reachableTypes.Add(rootEntityType);
typesToValidate.Enqueue(rootEntityType);
return;
AndriySvyryd marked this conversation as resolved.
Show resolved Hide resolved
}

while (typesToValidate.Count > 0)
var graph = new Multigraph<IEntityType, IForeignKey>();
foreach (var entityType in model.GetEntityTypes())
{
var primaryKey = entityType.FindPrimaryKey();
if (primaryKey == null)
{
var entityType = typesToValidate.Dequeue();
var primaryKey = entityType.FindPrimaryKey();
if (primaryKey == null)
{
continue;
}
continue;
}

foreach (var foreignKey in entityType.GetForeignKeys())
foreach (var foreignKey in entityType.GetForeignKeys())
{
var principalType = foreignKey.PrincipalEntityType;
if (!foreignKey.PrincipalKey.IsPrimaryKey()
|| !PropertyListComparer.Instance.Equals(foreignKey.Properties, primaryKey.Properties)
|| foreignKey.PrincipalEntityType.IsAssignableFrom(entityType))
{
var principalType = foreignKey.PrincipalEntityType;
if (!foreignKey.PrincipalKey.IsPrimaryKey()
|| !unvalidatedEntityTypes.Contains(principalType)
|| foreignKey.PrincipalEntityType.IsAssignableFrom(entityType)
|| !PropertyListComparer.Instance.Equals(foreignKey.Properties, primaryKey.Properties))
{
continue;
}

if (!reachableTypes.Add(principalType))
{
throw new InvalidOperationException(CoreStrings.IdentifyingRelationshipCycle(rootEntityType.DisplayName()));
}

typesToValidate.Enqueue(principalType);
continue;
}
}

foreach (var entityType in reachableTypes)
{
unvalidatedEntityTypes.Remove(entityType);
graph.AddVertex(entityType);
graph.AddVertex(principalType);
graph.AddEdge(entityType, principalType, foreignKey);
}
}

graph.TopologicalSort(
tryBreakEdge: null,
formatCycle: c => c.Select(d => d.Item1.DisplayName()).Join(" -> "),
c => CoreStrings.IdentifyingRelationshipCycle(c));
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore/Properties/CoreStrings.Designer.cs

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

2 changes: 1 addition & 1 deletion src/EFCore/Properties/CoreStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@
<value>The block size used for Hi-Lo value generation must be positive. When the Hi-Lo generator is backed by a SQL sequence this means that the sequence increment must be positive.</value>
</data>
<data name="IdentifyingRelationshipCycle" xml:space="preserve">
<value>The entity type '{entityType}' is part of a relationship cycle involving its primary key.</value>
<value>A relationship cycle involving the primary keys of the following enitity types was detected: '{entityType}'. This would prevent any entity to be inserted without violating the store constraints. Review the foreign keys defined on the primary keys and either remove or use other properties for at least one of them.</value>
</data>
<data name="IdentityConflict" xml:space="preserve">
<value>The instance of entity type '{entityType}' cannot be tracked because another instance with the same key value for {keyProperties} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.</value>
Expand Down
13 changes: 9 additions & 4 deletions src/Shared/Multigraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ public IReadOnlyList<TVertex> TopologicalSort(

public IReadOnlyList<TVertex> TopologicalSort(
[CanBeNull] Func<TVertex, TVertex, IEnumerable<TEdge>, bool> tryBreakEdge,
[CanBeNull] Func<IReadOnlyList<Tuple<TVertex, TVertex, IEnumerable<TEdge>>>, string> formatCycle)
[CanBeNull] Func<IReadOnlyList<Tuple<TVertex, TVertex, IEnumerable<TEdge>>>, string> formatCycle,
Func<string, string> formatException = null)
{
var sortedQueue = new List<TVertex>();
var predecessorCounts = new Dictionary<TVertex, int>();
Expand Down Expand Up @@ -248,15 +249,18 @@ public IReadOnlyList<TVertex> TopologicalSort(

cycle.Reverse();

ThrowCycle(cycle, formatCycle);
ThrowCycle(cycle, formatCycle, formatException);
}
}
}

return sortedQueue;
}

private void ThrowCycle(List<TVertex> cycle, Func<IReadOnlyList<Tuple<TVertex, TVertex, IEnumerable<TEdge>>>, string> formatCycle)
private void ThrowCycle(
List<TVertex> cycle,
Func<IReadOnlyList<Tuple<TVertex, TVertex, IEnumerable<TEdge>>>, string> formatCycle,
Func<string, string> formatException = null)
{
string cycleString;
if (formatCycle == null)
Expand All @@ -277,7 +281,8 @@ private void ThrowCycle(List<TVertex> cycle, Func<IReadOnlyList<Tuple<TVertex, T
cycleString = formatCycle(cycleData);
}

throw new InvalidOperationException(CoreStrings.CircularDependency(cycleString));
var message = formatException == null ? CoreStrings.CircularDependency(cycleString) : formatException(cycleString);
throw new InvalidOperationException(message);
}

protected virtual string ToString(TVertex vertex)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ public virtual void Detects_shared_table_root_cycle()
modelBuilder.Entity<B>().ToTable("Table");

VerifyError(
CoreStrings.IdentifyingRelationshipCycle(nameof(A)),
CoreStrings.IdentifyingRelationshipCycle("A -> B"),
modelBuilder.Model);
}

Expand Down
19 changes: 17 additions & 2 deletions test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
Expand Down Expand Up @@ -321,10 +321,25 @@ public virtual void Detects_relationship_cycle()
modelBuilder.Entity<C>().HasOne<B>().WithOne().HasForeignKey<B>(a => a.Id).HasPrincipalKey<C>(b => b.Id).IsRequired();

VerifyError(
CoreStrings.IdentifyingRelationshipCycle(nameof(A)),
CoreStrings.IdentifyingRelationshipCycle("A -> B -> C"),
modelBuilder.Model);
}

[ConditionalFact]
public virtual void Passes_on_multiple_relationship_paths()
{
var modelBuilder = base.CreateConventionalModelBuilder();

modelBuilder.Entity<A>();
modelBuilder.Entity<B>();
modelBuilder.Entity<C>().HasBaseType((string)null);
modelBuilder.Entity<A>().HasOne<B>().WithOne().HasForeignKey<A>(a => a.Id).HasPrincipalKey<B>(b => b.Id).IsRequired();
modelBuilder.Entity<A>().HasOne<C>().WithOne().HasForeignKey<A>(a => a.Id).HasPrincipalKey<C>(b => b.Id).IsRequired();
modelBuilder.Entity<C>().HasOne<B>().WithOne().HasForeignKey<B>(a => a.Id).HasPrincipalKey<C>(b => b.Id).IsRequired();

Validate(modelBuilder.Model);
}

[ConditionalFact]
public virtual void Passes_on_redundant_foreign_key()
{
Expand Down