Skip to content

Commit b5fbba4

Browse files
committed
Avoid throwing relationship severed exceptions until SaveChanges is finished
Fixes #30122 Because the entities for which the relationship is being severed may end up being deleted later on in SaveChanges.
1 parent 0b8a260 commit b5fbba4

File tree

9 files changed

+377
-165
lines changed

9 files changed

+377
-165
lines changed

src/EFCore/ChangeTracking/Internal/ChangeDetector.cs

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -117,28 +117,42 @@ public virtual void DetectChanges(IStateManager stateManager)
117117

118118
_logger.DetectChangesStarting(stateManager.Context);
119119

120-
foreach (var entry in stateManager.ToList()) // Might be too big, but usually _all_ entities are using Snapshot tracking
120+
try
121121
{
122-
switch (entry.EntityState)
123-
{
124-
case EntityState.Detached:
125-
break;
126-
case EntityState.Deleted:
127-
if (entry.SharedIdentityEntry != null)
128-
{
129-
continue;
130-
}
131-
132-
goto default;
133-
default:
134-
if (LocalDetectChanges(entry))
135-
{
136-
changesFound = true;
137-
}
122+
stateManager.PostponeConceptualNullExceptions = true;
138123

139-
break;
124+
foreach (var entry in stateManager.ToList()) // Might be too big, but usually _all_ entities are using Snapshot tracking
125+
{
126+
switch (entry.EntityState)
127+
{
128+
case EntityState.Detached:
129+
break;
130+
case EntityState.Deleted:
131+
if (entry.SharedIdentityEntry != null)
132+
{
133+
continue;
134+
}
135+
136+
goto default;
137+
default:
138+
if (LocalDetectChanges(entry))
139+
{
140+
changesFound = true;
141+
}
142+
143+
break;
144+
}
140145
}
141146
}
147+
finally
148+
{
149+
stateManager.PostponeConceptualNullExceptions = false;
150+
}
151+
152+
if (stateManager.DeleteOrphansTiming == CascadeTiming.Immediate)
153+
{
154+
stateManager.HandleConceptualNulls(false);
155+
}
142156

143157
_logger.DetectChangesCompleted(stateManager.Context);
144158

src/EFCore/ChangeTracking/Internal/IStateManager.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,22 @@ void SetEvents(
562562
/// </summary>
563563
void CascadeChanges(bool force);
564564

565+
/// <summary>
566+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
567+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
568+
/// any release. You should only use it directly in your code with extreme caution and knowing that
569+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
570+
/// </summary>
571+
void HandleConceptualNulls(bool force);
572+
573+
/// <summary>
574+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
575+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
576+
/// any release. You should only use it directly in your code with extreme caution and knowing that
577+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
578+
/// </summary>
579+
bool PostponeConceptualNullExceptions { get; set; }
580+
565581
/// <summary>
566582
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
567583
/// the same compatibility standards as public APIs. It may be changed or removed without notice in

src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1708,26 +1708,27 @@ public void HandleConceptualNulls(bool sensitiveLoggingEnabled, bool force, bool
17081708

17091709
SetEntityState(cascadeState);
17101710
}
1711-
else if (fks.Count > 0)
1711+
else if (!StateManager.PostponeConceptualNullExceptions)
17121712
{
1713-
var foreignKey = fks.First();
1714-
1715-
if (sensitiveLoggingEnabled)
1713+
if (fks.Count > 0)
17161714
{
1715+
var foreignKey = fks.First();
1716+
1717+
if (sensitiveLoggingEnabled)
1718+
{
1719+
throw new InvalidOperationException(
1720+
CoreStrings.RelationshipConceptualNullSensitive(
1721+
foreignKey.PrincipalEntityType.DisplayName(),
1722+
EntityType.DisplayName(),
1723+
this.BuildOriginalValuesString(foreignKey.Properties)));
1724+
}
1725+
17171726
throw new InvalidOperationException(
1718-
CoreStrings.RelationshipConceptualNullSensitive(
1727+
CoreStrings.RelationshipConceptualNull(
17191728
foreignKey.PrincipalEntityType.DisplayName(),
1720-
EntityType.DisplayName(),
1721-
this.BuildOriginalValuesString(foreignKey.Properties)));
1729+
EntityType.DisplayName()));
17221730
}
17231731

1724-
throw new InvalidOperationException(
1725-
CoreStrings.RelationshipConceptualNull(
1726-
foreignKey.PrincipalEntityType.DisplayName(),
1727-
EntityType.DisplayName()));
1728-
}
1729-
else
1730-
{
17311732
var property = EntityType.GetProperties().FirstOrDefault(
17321733
p => (EntityState != EntityState.Modified
17331734
|| IsModified(p))

src/EFCore/ChangeTracking/Internal/StateManager.cs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,6 +1162,22 @@ public virtual void CascadeChanges(bool force)
11621162
{
11631163
// Perf sensitive
11641164

1165+
HandleConceptualNulls(force);
1166+
1167+
foreach (var entry in this.ToListForState(deleted: true))
1168+
{
1169+
CascadeDelete(entry, force);
1170+
}
1171+
}
1172+
1173+
/// <summary>
1174+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
1175+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
1176+
/// any release. You should only use it directly in your code with extreme caution and knowing that
1177+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
1178+
/// </summary>
1179+
public virtual void HandleConceptualNulls(bool force)
1180+
{
11651181
var toHandle = new List<InternalEntityEntry>();
11661182

11671183
foreach (var entry in GetEntriesForState(modified: true, added: true))
@@ -1176,13 +1192,16 @@ public virtual void CascadeChanges(bool force)
11761192
{
11771193
entry.HandleConceptualNulls(SensitiveLoggingEnabled, force, isCascadeDelete: false);
11781194
}
1179-
1180-
foreach (var entry in this.ToListForState(deleted: true))
1181-
{
1182-
CascadeDelete(entry, force);
1183-
}
11841195
}
11851196

1197+
/// <summary>
1198+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
1199+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
1200+
/// any release. You should only use it directly in your code with extreme caution and knowing that
1201+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
1202+
/// </summary>
1203+
public virtual bool PostponeConceptualNullExceptions { get; set; }
1204+
11861205
/// <summary>
11871206
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
11881207
/// the same compatibility standards as public APIs. It may be changed or removed without notice in

0 commit comments

Comments
 (0)