dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.79k stars 3.19k forks source link

EFCore 8: DbContext (StateManager) adds back previously detached entities #33557

Open Daniel-Svensson opened 7 months ago

Daniel-Svensson commented 7 months ago

After having detached entities from the DbContext in EF Core 8 and made sure that they are no longer part of ChangeTracker.Entries() they will still automatically be added back later if a later object has a FK or similar with their Id.

If attaching an entity with id X, detaching it and then later attaching it a InvalidOperationException will be thrown. If you on the other hand add a dependant/dependent on X a navigation fixup might happen against an instance which is not really part of the DbContext nor the Database...

The following problem is new in EF Core 8, it works fine with earlier version.

The behaviour was triggered by some code which generates temporary entities, attaches them to a DbContext (so that fixup of navigation properties) happens, runs some code and then detaches them (there is still some cached data in the context used for all runs). The temporary entities must sometimes reuse the same Id

Include your code

I expect the following code to work the same way as for EF Core 7, where the new object graph is attached without any problem.

// See https://aka.ms/new-console-template for more information

using System.Diagnostics;
using Microsoft.EntityFrameworkCore;

var a = new Flow { TransactionId = -2, Transaction = new Transaction { Id = -2, DealId = -3, Deal = new Deal() { Id = -3 } }};
var b = new Flow { TransactionId = -2, Transaction = new Transaction { Id = -2, DealId = -3, Deal = new Deal() { Id = -3 } } };

Connect(a);
Connect(b);

using var ctx = new SampleContext();

ctx.Flows.Add(a);

// Detach everything
foreach (var element in new object[] { a.Transaction.Deal, a.Transaction, a })
{
    ctx.Entry(element).State = EntityState.Detached;
}

//ctx.ChangeTracker.DebugView.ShortView.Dump("After Detatch");
Debug.Assert(ctx.ChangeTracker.Entries().Any() == false, "There are no entries tracked by changetracker");

/// HERE IT GOES ************BOOOOOOOOOOOOM********** InvalidOperationException 
ctx.Flows.Add(b);

void Connect(Flow f)
{
    f.TransactionId = f.Transaction!.Id;
    f.Transaction.Flows.Add(f);

    f.Transaction.DealId = f.Transaction.Deal!.Id;
    f.Transaction.Deal.Transactions.Add(f.Transaction);
}

class SampleContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseInMemoryDatabase("MyDatabase");
    }

    public virtual DbSet<Deal> Deals { get; set; }
    public virtual DbSet<Transaction> Transactions { get; set; }
    public virtual DbSet<Flow> Flows { get; set; }
}

internal class Deal
{
    public int Id { get; set; }
    public ICollection<Transaction> Transactions { get; set; } = new HashSet<Transaction>();
}

internal class Transaction
{
    public int Id { get; set; }
    public int DealId { get; set; }
    public Deal? Deal { get; set; }
    public ICollection<Flow> Flows { get; set; } = new HashSet<Flow>();
}

internal class Flow
{
    public int Id { get; set; }
    public int TransactionId { get; set; }
    public Transaction? Transaction { get; set; }
}

Include stack traces

Include the full exception message and stack trace for any exception you encounter.

Use triple-tick fences for stack traces. For example:

>   [Exception] Microsoft.EntityFrameworkCore.dll!Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap<TKey>.ThrowIdentityConflict(Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry entry) Line 283   C#
    [Exception] Microsoft.EntityFrameworkCore.dll!Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap<TKey>.Add(TKey key, Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry entry, bool updateDuplicate) Line 303 C#
    [Exception] Microsoft.EntityFrameworkCore.dll!Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap<TKey>.Add(TKey key, Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry entry) Line 255   C#
    [Exception] Microsoft.EntityFrameworkCore.dll!Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap<TKey>.Add(Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry entry) Line 237 C#
    [Exception] Microsoft.EntityFrameworkCore.dll!Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.StartTracking(Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry entry) Line 602    C#
    [Exception] Microsoft.EntityFrameworkCore.dll!Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(Microsoft.EntityFrameworkCore.EntityState oldState, Microsoft.EntityFrameworkCore.EntityState newState, bool acceptChanges, bool modifyProperties) Line 364  C#
    [Exception] Microsoft.EntityFrameworkCore.dll!Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(Microsoft.EntityFrameworkCore.EntityState entityState, bool acceptChanges, bool modifyProperties, Microsoft.EntityFrameworkCore.EntityState? forceStateWhenUnknownKey, Microsoft.EntityFrameworkCore.EntityState? fallbackState) Line 169    C#
    [Exception] Microsoft.EntityFrameworkCore.dll!Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.PaintAction(Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntryGraphNode<(Microsoft.EntityFrameworkCore.EntityState TargetState, Microsoft.EntityFrameworkCore.EntityState StoreGenTargetState, bool Force)> node) Line 125    C#
    [Exception] Microsoft.EntityFrameworkCore.dll!Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph<TState>(Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntryGraphNode<TState> node, System.Func<Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntryGraphNode<TState>, bool> handleNode) Line 26 C#
    [Exception] Microsoft.EntityFrameworkCore.dll!Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph<TState>(Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntryGraphNode<TState> node, System.Func<Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntryGraphNode<TState>, bool> handleNode) Line 49 C#
    [Exception] Microsoft.EntityFrameworkCore.dll!Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph<TState>(Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntryGraphNode<TState> node, System.Func<Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntryGraphNode<TState>, bool> handleNode) Line 57 C#
    [Exception] Microsoft.EntityFrameworkCore.dll!Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph<TState>(Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntryGraphNode<TState> node, System.Func<Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntryGraphNode<TState>, bool> handleNode) Line 57 C#
    [Exception] Microsoft.EntityFrameworkCore.dll!Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.AttachGraph(Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry rootEntry, Microsoft.EntityFrameworkCore.EntityState targetState, Microsoft.EntityFrameworkCore.EntityState storeGeneratedWithKeySetTargetState, bool forceStateWhenUnknownKey) Line 45   C#
    [Exception] Microsoft.EntityFrameworkCore.dll!Microsoft.EntityFrameworkCore.Internal.InternalDbSet<TEntity>.SetEntityState(Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry entry, Microsoft.EntityFrameworkCore.EntityState entityState) Line 571 C#
    [Exception] Microsoft.EntityFrameworkCore.dll!Microsoft.EntityFrameworkCore.Internal.InternalDbSet<TEntity>.Add(TEntity entity) Line 192    C#
    [Exception] efcore_repro.dll!Program.<Main>$(string[] args) Line 29 C#
    Microsoft.EntityFrameworkCore.dll!Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.AttachGraph(Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry rootEntry, Microsoft.EntityFrameworkCore.EntityState targetState, Microsoft.EntityFrameworkCore.EntityState storeGeneratedWithKeySetTargetState, bool forceStateWhenUnknownKey) Line 60   C#
    Microsoft.EntityFrameworkCore.dll!Microsoft.EntityFrameworkCore.Internal.InternalDbSet<System.__Canon>.SetEntityState(Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry entry, Microsoft.EntityFrameworkCore.EntityState entityState) Line 584  C#
    Microsoft.EntityFrameworkCore.dll!Microsoft.EntityFrameworkCore.Internal.InternalDbSet<Flow>.Add(Flow entity) Line 194  C#
    efcore_repro.dll!Program.<Main>$(string[] args) Line 29 C#

Include provider and version information

EF Core version: 8.0.4 Database provider: * Target framework: .NET 8.0 Operating system: windows 11 IDE: N/A

Daniel-Svensson commented 7 months ago

Analysis

The problematic code seems to be with the "_dependentMaps" and StateManager WHen "UnTrracking" an entity it seems like StateManager tries to remove it from from DependentMap .

Entity is NOT removed from dependent maps, which seems somewhat due to the TryCreateFromCurrentValues(entry, out var key) in DependentMap.Remove returning false (due to the Id property having been flagged as null). So the entry stays in the internal cache in stead of being removed on Detach.

The entry for "Id = -7" is still left in the map and not removed when it calls map.Remove() with an entry that has "Id = -7" image image

ajcvickers commented 6 months ago

Note for team: confirmed bug when iterating over tracked entities and detaching them. Note that this is typically anti-pattern. It is being done here because some other state is being maintained in the context between units of work.

SandstromErik commented 1 month ago

When is this bug expected to be fixed? In version 8 or version 9?