Skip to content

Commit

Permalink
Interceptor for identity resolution when attaching graphs (#28246)
Browse files Browse the repository at this point in the history
  • Loading branch information
ajcvickers authored Jun 20, 2022
1 parent 35a96c9 commit e09ad8b
Show file tree
Hide file tree
Showing 29 changed files with 1,849 additions and 94 deletions.
2 changes: 1 addition & 1 deletion src/EFCore.InMemory/Storage/Internal/InMemoryTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ public virtual void BumpValueGenerators(object?[] row)
}

private TKey CreateKey(IUpdateEntry entry)
=> _keyValueFactory.CreateFromCurrentValues(entry);
=> _keyValueFactory.CreateFromCurrentValues(entry)!;

private static object? SnapshotValue(IProperty property, ValueComparer? comparer, IUpdateEntry entry)
{
Expand Down
4 changes: 2 additions & 2 deletions src/EFCore/ChangeTracking/IPrincipalKeyValueFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public interface IPrincipalKeyValueFactory<TKey>
/// </summary>
/// <param name="entry">The entry tracking an entity instance.</param>
/// <returns>The key value.</returns>
TKey CreateFromCurrentValues(IUpdateEntry entry);
TKey? CreateFromCurrentValues(IUpdateEntry entry);

/// <summary>
/// Finds the first null key value in the given entry and returns the associated <see cref="IProperty" />.
Expand All @@ -59,7 +59,7 @@ public interface IPrincipalKeyValueFactory<TKey>
/// </summary>
/// <param name="entry">The entry tracking an entity instance.</param>
/// <returns>The key value.</returns>
TKey CreateFromOriginalValues(IUpdateEntry entry);
TKey? CreateFromOriginalValues(IUpdateEntry entry);

/// <summary>
/// Creates a key object from the relationship snapshot key values in the given entry.
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore/ChangeTracking/Internal/DependentsMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ private bool TryCreateFromCurrentValues(IUpdateEntry entry, [NotNullWhen(true)]
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual IEnumerable<IUpdateEntry> GetDependents(IUpdateEntry principalEntry)
=> _map.TryGetValue(_principalKeyValueFactory.CreateFromCurrentValues(principalEntry), out var dependents)
=> _map.TryGetValue(_principalKeyValueFactory.CreateFromCurrentValues(principalEntry)!, out var dependents)
? dependents
: Enumerable.Empty<IUpdateEntry>();

Expand Down
61 changes: 43 additions & 18 deletions src/EFCore/ChangeTracking/Internal/EntityGraphAttacher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ namespace Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
public class EntityGraphAttacher : IEntityGraphAttacher
{
private readonly IEntityEntryGraphIterator _graphIterator;
private HashSet<object>? _visited;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down Expand Up @@ -49,10 +50,12 @@ public virtual void AttachGraph(
null),
PaintAction);

_visited = null;
rootEntry.StateManager.CompleteAttachGraph();
}
catch
{
_visited = null;
rootEntry.StateManager.AbortAttachGraph();
throw;
}
Expand Down Expand Up @@ -84,22 +87,25 @@ await _graphIterator.TraverseGraphAsync(
PaintActionAsync,
cancellationToken).ConfigureAwait(false);

_visited = null;
rootEntry.StateManager.CompleteAttachGraph();
}
catch
{
_visited = null;
rootEntry.StateManager.AbortAttachGraph();
throw;
}
}

private static bool PaintAction(
private bool PaintAction(
EntityEntryGraphNode<(EntityState TargetState, EntityState StoreGenTargetState, bool Force)> node)
{
SetReferenceLoaded(node);

var internalEntityEntry = node.GetInfrastructure();
if (internalEntityEntry.EntityState != EntityState.Detached)
if (internalEntityEntry.EntityState != EntityState.Detached
|| (_visited != null && _visited.Contains(internalEntityEntry.Entity)))
{
return false;
}
Expand All @@ -108,24 +114,34 @@ private static bool PaintAction(

var (isGenerated, isSet) = internalEntityEntry.IsKeySet;

internalEntityEntry.SetEntityState(
isSet
? (isGenerated ? storeGenTargetState : targetState)
: EntityState.Added, // Key can only be not-set if it is store-generated
acceptChanges: true,
forceStateWhenUnknownKey: force ? targetState : null);
if (internalEntityEntry.StateManager.ResolveToExistingEntry(
internalEntityEntry,
node.InboundNavigation, node.SourceEntry?.GetInfrastructure()))
{
(_visited ??= new HashSet<object>(ReferenceEqualityComparer.Instance)).Add(internalEntityEntry.Entity);
}
else
{
internalEntityEntry.SetEntityState(
isSet
? (isGenerated ? storeGenTargetState : targetState)
: EntityState.Added, // Key can only be not-set if it is store-generated
acceptChanges: true,
forceStateWhenUnknownKey: force ? targetState : null);
}

return true;
}

private static async Task<bool> PaintActionAsync(
private async Task<bool> PaintActionAsync(
EntityEntryGraphNode<(EntityState TargetState, EntityState StoreGenTargetState, bool Force)> node,
CancellationToken cancellationToken)
{
SetReferenceLoaded(node);

var internalEntityEntry = node.GetInfrastructure();
if (internalEntityEntry.EntityState != EntityState.Detached)
if (internalEntityEntry.EntityState != EntityState.Detached
|| (_visited != null && _visited.Contains(internalEntityEntry.Entity)))
{
return false;
}
Expand All @@ -134,14 +150,23 @@ private static async Task<bool> PaintActionAsync(

var (isGenerated, isSet) = internalEntityEntry.IsKeySet;

await internalEntityEntry.SetEntityStateAsync(
isSet
? (isGenerated ? storeGenTargetState : targetState)
: EntityState.Added, // Key can only be not-set if it is store-generated
acceptChanges: true,
forceStateWhenUnknownKey: force ? targetState : null,
cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (internalEntityEntry.StateManager.ResolveToExistingEntry(
internalEntityEntry,
node.InboundNavigation, node.SourceEntry?.GetInfrastructure()))
{
(_visited ??= new HashSet<object>()).Add(internalEntityEntry.Entity);
}
else
{
await internalEntityEntry.SetEntityStateAsync(
isSet
? (isGenerated ? storeGenTargetState : targetState)
: EntityState.Added, // Key can only be not-set if it is store-generated
acceptChanges: true,
forceStateWhenUnknownKey: force ? targetState : null,
cancellationToken: cancellationToken)
.ConfigureAwait(false);
}

return true;
}
Expand Down
8 changes: 8 additions & 0 deletions src/EFCore/ChangeTracking/Internal/IIdentityMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ public interface IIdentityMap
/// </summary>
bool Contains(IForeignKey foreignKey, in ValueBuffer valueBuffer);

/// <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>
InternalEntityEntry? TryGetEntry(InternalEntityEntry entry);

/// <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
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ public interface IInternalEntityEntryNotifier
/// </summary>
void StateChanged(InternalEntityEntry entry, EntityState oldState, bool fromQuery);

/// <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 FixupResolved(InternalEntityEntry entry, InternalEntityEntry duplicateEntry);

/// <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
10 changes: 10 additions & 0 deletions src/EFCore/ChangeTracking/Internal/INavigationFixer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,14 @@ void StateChanged(
InternalEntityEntry entry,
EntityState oldState,
bool fromQuery);

/// <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 FixupResolved(
InternalEntityEntry entry,
InternalEntityEntry duplicateEntry);
}
23 changes: 23 additions & 0 deletions src/EFCore/ChangeTracking/Internal/IStateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,29 @@ void RecordReferencedUntrackedEntity(
INavigationBase navigation,
InternalEntityEntry referencedFromEntry);

/// <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 UpdateReferencedUntrackedEntity(
object referencedEntity,
object newReferencedEntity,
INavigationBase navigation,
InternalEntityEntry referencedFromEntry);

/// <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 ResolveToExistingEntry(
InternalEntityEntry newEntry,
INavigationBase? navigation,
InternalEntityEntry? referencedFromEntry);

/// <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
20 changes: 17 additions & 3 deletions src/EFCore/ChangeTracking/Internal/IdentityMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@ public virtual bool Contains(IForeignKey foreignKey, in ValueBuffer valueBuffer)
=> foreignKey.GetDependentKeyValueFactory<TKey>()!.TryCreateFromBuffer(valueBuffer, out var key)
&& _identityMap.ContainsKey(key);

/// <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 InternalEntityEntry? TryGetEntry(InternalEntityEntry entry)
{
var key = PrincipalKeyValueFactory.CreateFromCurrentValues(entry);
return key != null && _identityMap.TryGetValue(key, out var existingEntry)
? existingEntry
: null;
}

/// <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 Expand Up @@ -194,7 +208,7 @@ public virtual bool Contains(IForeignKey foreignKey, in ValueBuffer valueBuffer)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual void AddOrUpdate(InternalEntityEntry entry)
=> Add(PrincipalKeyValueFactory.CreateFromCurrentValues(entry), entry, updateDuplicate: true);
=> Add(PrincipalKeyValueFactory.CreateFromCurrentValues(entry)!, entry, updateDuplicate: true);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -203,7 +217,7 @@ public virtual void AddOrUpdate(InternalEntityEntry entry)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual void Add(InternalEntityEntry entry)
=> Add(PrincipalKeyValueFactory.CreateFromCurrentValues(entry), entry);
=> Add(PrincipalKeyValueFactory.CreateFromCurrentValues(entry)!, entry);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down Expand Up @@ -371,7 +385,7 @@ public virtual void Clear()
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual void Remove(InternalEntityEntry entry)
=> Remove(PrincipalKeyValueFactory.CreateFromCurrentValues(entry), entry);
=> Remove(PrincipalKeyValueFactory.CreateFromCurrentValues(entry)!, entry);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
11 changes: 11 additions & 0 deletions src/EFCore/ChangeTracking/Internal/InternalEntityEntryNotifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ public virtual void StateChanged(InternalEntityEntry entry, EntityState oldState
_localViewListener.StateChanged(entry, oldState, fromQuery);
}

/// <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 FixupResolved(
InternalEntityEntry entry,
InternalEntityEntry duplicateEntry)
=> _navigationFixer.FixupResolved(entry, duplicateEntry);

/// <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 e09ad8b

Please sign in to comment.