Skip to content

Commit

Permalink
Avoid throwing relationship severed exceptions until SaveChanges is f…
Browse files Browse the repository at this point in the history
…inished

Fixes #30122

Because the entities for which the relationship is being severed may end up being deleted later on in SaveChanges.
  • Loading branch information
ajcvickers committed Jan 31, 2023
1 parent 0b8a260 commit b5fbba4
Show file tree
Hide file tree
Showing 9 changed files with 377 additions and 165 deletions.
50 changes: 32 additions & 18 deletions src/EFCore/ChangeTracking/Internal/ChangeDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,28 +117,42 @@ public virtual void DetectChanges(IStateManager stateManager)

_logger.DetectChangesStarting(stateManager.Context);

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;
}

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

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;
}
}
}
finally
{
stateManager.PostponeConceptualNullExceptions = false;
}

if (stateManager.DeleteOrphansTiming == CascadeTiming.Immediate)
{
stateManager.HandleConceptualNulls(false);
}

_logger.DetectChangesCompleted(stateManager.Context);

Expand Down
16 changes: 16 additions & 0 deletions src/EFCore/ChangeTracking/Internal/IStateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,22 @@ void SetEvents(
/// </summary>
void CascadeChanges(bool force);

/// <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>
void HandleConceptualNulls(bool force);

/// <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>
bool PostponeConceptualNullExceptions { 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
Expand Down
29 changes: 15 additions & 14 deletions src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1708,26 +1708,27 @@ public void HandleConceptualNulls(bool sensitiveLoggingEnabled, bool force, bool

SetEntityState(cascadeState);
}
else if (fks.Count > 0)
else if (!StateManager.PostponeConceptualNullExceptions)
{
var foreignKey = fks.First();

if (sensitiveLoggingEnabled)
if (fks.Count > 0)
{
var foreignKey = fks.First();

if (sensitiveLoggingEnabled)
{
throw new InvalidOperationException(
CoreStrings.RelationshipConceptualNullSensitive(
foreignKey.PrincipalEntityType.DisplayName(),
EntityType.DisplayName(),
this.BuildOriginalValuesString(foreignKey.Properties)));
}

throw new InvalidOperationException(
CoreStrings.RelationshipConceptualNullSensitive(
CoreStrings.RelationshipConceptualNull(
foreignKey.PrincipalEntityType.DisplayName(),
EntityType.DisplayName(),
this.BuildOriginalValuesString(foreignKey.Properties)));
EntityType.DisplayName()));
}

throw new InvalidOperationException(
CoreStrings.RelationshipConceptualNull(
foreignKey.PrincipalEntityType.DisplayName(),
EntityType.DisplayName()));
}
else
{
var property = EntityType.GetProperties().FirstOrDefault(
p => (EntityState != EntityState.Modified
|| IsModified(p))
Expand Down
29 changes: 24 additions & 5 deletions src/EFCore/ChangeTracking/Internal/StateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1162,6 +1162,22 @@ public virtual void CascadeChanges(bool force)
{
// Perf sensitive

HandleConceptualNulls(force);

foreach (var entry in this.ToListForState(deleted: true))
{
CascadeDelete(entry, force);
}
}

/// <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 void HandleConceptualNulls(bool force)
{
var toHandle = new List<InternalEntityEntry>();

foreach (var entry in GetEntriesForState(modified: true, added: true))
Expand All @@ -1176,13 +1192,16 @@ public virtual void CascadeChanges(bool force)
{
entry.HandleConceptualNulls(SensitiveLoggingEnabled, force, isCascadeDelete: false);
}

foreach (var entry in this.ToListForState(deleted: true))
{
CascadeDelete(entry, force);
}
}

/// <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 PostponeConceptualNullExceptions { 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
Expand Down
Loading

0 comments on commit b5fbba4

Please sign in to comment.