Skip to content

Commit

Permalink
Prevent re-entrance into DetectChanges
Browse files Browse the repository at this point in the history
Fixes #30122
Fixes #30135

EF7 contained some new calls to local `DetectChanges`. This resulted in re-entrance into `DetectChanges` for some types of graphs. This change prevents that.
  • Loading branch information
ajcvickers committed Feb 14, 2023
1 parent eb28fd3 commit b4968ff
Show file tree
Hide file tree
Showing 10 changed files with 1,066 additions and 332 deletions.
80 changes: 55 additions & 25 deletions src/EFCore/ChangeTracking/Internal/ChangeDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public class ChangeDetector : IChangeDetector
{
private readonly IDiagnosticsLogger<DbLoggerCategory.ChangeTracking> _logger;
private readonly ILoggingOptions _loggingOptions;
private bool _inCascadeDelete;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down Expand Up @@ -112,37 +113,51 @@ public virtual void PropertyChanging(InternalEntityEntry entry, IPropertyBase pr
/// </summary>
public virtual void DetectChanges(IStateManager stateManager)
{
OnDetectingAllChanges(stateManager);
var changesFound = false;

_logger.DetectChangesStarting(stateManager.Context);
if (_inCascadeDelete)
{
return;
}

foreach (var entry in stateManager.ToList()) // Might be too big, but usually _all_ entities are using Snapshot tracking
try
{
switch (entry.EntityState)
{
case EntityState.Detached:
break;
case EntityState.Deleted:
if (entry.SharedIdentityEntry != null)
{
continue;
}
_inCascadeDelete = true;

goto default;
default:
if (LocalDetectChanges(entry))
{
changesFound = true;
}
OnDetectingAllChanges(stateManager);
var changesFound = false;

_logger.DetectChangesStarting(stateManager.Context);

break;
foreach (var entry in stateManager.ToList()) // Might be too big, but usually _all_ entities are using Snapshot tracking
{
switch (entry.EntityState)
{
case EntityState.Detached:
break;
case EntityState.Deleted:
if (entry.SharedIdentityEntry != null)
{
continue;
}

goto default;
default:
if (LocalDetectChanges(entry))
{
changesFound = true;
}

break;
}
}
}

_logger.DetectChangesCompleted(stateManager.Context);
_logger.DetectChangesCompleted(stateManager.Context);

OnDetectedAllChanges(stateManager, changesFound);
OnDetectedAllChanges(stateManager, changesFound);
}
finally
{
_inCascadeDelete = false;
}
}

/// <summary>
Expand All @@ -152,7 +167,22 @@ public virtual void DetectChanges(IStateManager stateManager)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual void DetectChanges(InternalEntityEntry entry)
=> DetectChanges(entry, new HashSet<InternalEntityEntry> { entry });
{
if (_inCascadeDelete)
{
return;
}

try
{
_inCascadeDelete = true;
DetectChanges(entry, new HashSet<InternalEntityEntry> { entry });
}
finally
{
_inCascadeDelete = false;
}
}

private bool DetectChanges(InternalEntityEntry entry, HashSet<InternalEntityEntry> visited)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// 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;

public class GraphUpdatesIdentityResolutionInMemoryTest
: GraphUpdatesInMemoryTestBase<GraphUpdatesIdentityResolutionInMemoryTest.InMemoryIdentityResolutionFixture>
{
public GraphUpdatesIdentityResolutionInMemoryTest(InMemoryIdentityResolutionFixture fixture)
: base(fixture)
{
}

[ConditionalFact]
public void Can_attach_full_required_graph_of_duplicates()
=> ExecuteWithStrategyInTransaction(
context =>
{
var trackedRoot = LoadRequiredGraph(context);
var entries = context.ChangeTracker.Entries().ToList();
context.Attach(QueryRequiredGraph(context).AsNoTracking().Single(IsTheRoot));
AssertEntries(entries, context.ChangeTracker.Entries().ToList());
AssertNavigations(trackedRoot);
Assert.Equal(0, context.SaveChanges());
});

[ConditionalFact]
public void Can_attach_full_optional_graph_of_duplicates()
=> ExecuteWithStrategyInTransaction(
context =>
{
var trackedRoot = LoadOptionalGraph(context);
var entries = context.ChangeTracker.Entries().ToList();
context.Attach(QueryOptionalGraph(context).AsNoTracking().Single(IsTheRoot));
AssertEntries(entries, context.ChangeTracker.Entries().ToList());
AssertNavigations(trackedRoot);
Assert.Equal(0, context.SaveChanges());
});

[ConditionalFact]
public void Can_attach_full_required_non_PK_graph_of_duplicates()
=> ExecuteWithStrategyInTransaction(
context =>
{
var trackedRoot = LoadRequiredNonPkGraph(context);
var entries = context.ChangeTracker.Entries().ToList();
context.Attach(QueryRequiredNonPkGraph(context).AsNoTracking().Single(IsTheRoot));
AssertEntries(entries, context.ChangeTracker.Entries().ToList());
AssertNavigations(trackedRoot);
Assert.Equal(0, context.SaveChanges());
});

[ConditionalFact]
public void Can_attach_full_required_AK_graph_of_duplicates()
=> ExecuteWithStrategyInTransaction(
context =>
{
var trackedRoot = LoadRequiredAkGraph(context);
var entries = context.ChangeTracker.Entries().ToList();
context.Attach(QueryRequiredAkGraph(context).AsNoTracking().Single(IsTheRoot));
AssertEntries(entries, context.ChangeTracker.Entries().ToList());
AssertNavigations(trackedRoot);
Assert.Equal(0, context.SaveChanges());
});

[ConditionalFact]
public void Can_attach_full_optional_AK_graph_of_duplicates()
=> ExecuteWithStrategyInTransaction(
context =>
{
var trackedRoot = LoadOptionalAkGraph(context);
var entries = context.ChangeTracker.Entries().ToList();
context.Attach(QueryOptionalAkGraph(context).AsNoTracking().Single(IsTheRoot));
AssertEntries(entries, context.ChangeTracker.Entries().ToList());
AssertNavigations(trackedRoot);
Assert.Equal(0, context.SaveChanges());
});

[ConditionalFact]
public void Can_attach_full_required_non_PK_AK_graph_of_duplicates()
=> ExecuteWithStrategyInTransaction(
context =>
{
var trackedRoot = LoadRequiredNonPkAkGraph(context);
var entries = context.ChangeTracker.Entries().ToList();
context.Attach(QueryRequiredNonPkAkGraph(context).AsNoTracking().Single(IsTheRoot));
AssertEntries(entries, context.ChangeTracker.Entries().ToList());
AssertNavigations(trackedRoot);
Assert.Equal(0, context.SaveChanges());
});

[ConditionalFact]
public void Can_attach_full_required_one_to_many_graph_of_duplicates()
=> ExecuteWithStrategyInTransaction(
context =>
{
var trackedRoot = LoadOptionalOneToManyGraph(context);
var entries = context.ChangeTracker.Entries().ToList();
context.Attach(QueryOptionalOneToManyGraph(context).AsNoTracking().Single(IsTheRoot));
AssertEntries(entries, context.ChangeTracker.Entries().ToList());
AssertNavigations(trackedRoot);
Assert.Equal(0, context.SaveChanges());
});

[ConditionalFact]
public void Can_attach_full_required_composite_graph_of_duplicates()
=> ExecuteWithStrategyInTransaction(
context =>
{
var trackedRoot = LoadRequiredCompositeGraph(context);
var entries = context.ChangeTracker.Entries().ToList();
context.Attach(QueryRequiredCompositeGraph(context).AsNoTracking().Single(IsTheRoot));
AssertEntries(entries, context.ChangeTracker.Entries().ToList());
AssertNavigations(trackedRoot);
Assert.Equal(0, context.SaveChanges());
});

public class InMemoryIdentityResolutionFixture : GraphUpdatesInMemoryFixtureBase
{
protected override string StoreName
=> "GraphUpdatesIdentityResolutionTest";

public override bool HasIdentityResolution
=> true;

public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
=> base.AddOptions(builder).AddInterceptors(new UpdatingIdentityResolutionInterceptor());
}
}
Loading

0 comments on commit b4968ff

Please sign in to comment.