Home > front end >  EF Core: the instance of entity type [EntityType] cannot be tracked because another instance with th
EF Core: the instance of entity type [EntityType] cannot be tracked because another instance with th

Time:01-27

In my learning project, while using EF Core, I'm following the repository pattern and I'm using DI to inject the context for each repository.

The context is registered as follows.All services and repositories are transient.

builder.Services.AddDbContext<AppContext>(options => { 
     options.UseSqlServer(builder.Configuration.GetConnectionString("ConnectionString"));
     options.EnableSensitiveDataLogging();
});

I added sensitive data logging as an attempt to debug this but to no avail so far.

The exception appears when I attempt to use:

_context.Update(playableCharacter)  
await _context.SaveChangesAsync();

There is a static list I use to store the playableCharacter until all changes are finished and it's ready to be saved in the database again.

Here is my controller:

public async Task<ActionResult> CommitPlayerAction()
{
    var userId = _userService.GetUserId(User); //Retrieves user ID from ClaimsPrincipal
    var activeGameInstance = await _userParagraphRepository.GetActiveByUserIdNoTrackingAsync(_userService.GetUserId(User)); //Call to repository 

    // Call to static list storing fight instances. They aren't supposed to be saved in DB. 
    var activeFightInstance = _fightService.GetActiveFightInstance(userId, activeGameInstance.ActiveCharacter.CharacterId); 
    await _fightService.CommitAction(userId); // Manage the fight state   Makes call to playable character repository for update. That's where the issue arises.

    // Retrieves fight state for given request from the static list
    var fightState = _fightService.GetFightState(_userService.GetUserId(User),activeGameInstance.ActiveCharacter.CharacterId);
    activeFightInstance.ActionGrantedByItem = false;

    _fightService.ResetActiveTarget();
    _fightService.ResetActiveAction();
}

The service layer:

public async Task CommitAction(string userId)
{
    /*Game logic I cut off for clarity, mostly changes on playable character and his enemies */ 

    var combatEnded = IsFightFinished(_activeFightInstance.ActiveEnemies, GetActivePlayer()); 

    if (combatEnded) 
    {
        var fightWon = IsFightWon(_activeFightInstance.ActivePlayer);
        FinishFight(fightWon);

        await _playableCharacterRepository.UpdateAsync(_activeFightInstance.ActivePlayer);
    }
    else
    {
        // Some game logic
    }
}

Service layer dependencies:

private IFightRepository _fightRepository; 
private FightInstance _activeFightInstance;
private IFightFactory _fightFactory;
private IUserParagraphRepository _userParagraphRepository;
private ICharacterFactory _characterFactory;
private readonly IPlayableCharacterRepository _playableCharacterRepository; 

public FightService(IFightRepository fightRepository,
            IFightFactory fightFactory,
            IUserParagraphRepository userParagraphRepository,
            ICharacterFactory characterFactory,
            IPlayableCharacterRepository playableCharacterRepository)
{  
    _fightRepository = fightRepository;   
    _fightFactory = fightFactory;
    _userParagraphRepository = userParagraphRepository;
    _characterFactory = characterFactory;
    _playableCharacterRepository = playableCharacterRepository;
}
    
public FightInstance GetActiveFightInstance(string userId, int characterId)
{
    // This fight instance stores a reference to our playable character in the static list to share with the entire service. 
    _activeFightInstance = _fightRepository.GetById(userId, characterId);
    return _activeFightInstance;
}

"Game instance" repository:

public async Task<UserParagraph> GetActiveByUserIdNoTrackingAsync(string userId)
{
    return await _context.UserParagraphs
                .Include(x => x.ActiveCharacter)
                .Include(x => x.Paragraph)
                    .ThenInclude(p => p.Choices)
                .Include(x => x.Paragraph)
                    .ThenInclude(x => x.TestProp)
                .Include(x => x.Paragraph)
                    .ThenInclude(x => x.FightProp)
                    .ThenInclude(y => y.ParagraphEnemies)
                    .ThenInclude(z => z.Enemy)
                .Include(x => x.ActiveCharacter)
                .AsNoTracking()
                .SingleOrDefaultAsync(s => s.User.Id == userId && s.ActiveGame); 
}

Fight repository (with the static list that might be causing issues)

internal  class FightRepository : IFightRepository
{
    // I use a List instead of a dictionary as I need to have non unique records inside
    private static List<KeyValuePair<string, FightInstance>> FightInstances { get; set; } = new List<KeyValuePair<string, FightInstance>>();

The entity I'm trying to update:

[Key]
public int CharacterId { get; set; }
public bool IsTemplate { get; set; }
public string UserId { get; set; }//Id of character owner
public User User { get; set; } 
public UserParagraph? UserParagraph { get; set; } //Game instance in the form of a many to many relationship between the user and " paragraphs".
public int? UserParagraphId { get; set; } //Nullable as a character can be an instance or a template for other users. It has to remain  like this. 
public PlayableRace Race { get; set; }  
public int RaceId { get; set; } 
public PlayableClass CharacterClass { get; set; }  
public int PlayableClassId { get; set; } 
Many to many 
        //=============================================================// 
//Those entities are causing issues , they might be removed or changed and I won't know about it when updating.  
//That's why the "Update" method seemed perfect.  

public List<ActionCharacter>? LinkedActions { get; set; }    
public List<ItemCharacter>? LinkedItems { get; set; } 
        //=============================================================//

It's a possible duplicate of this question however after trying everything I can't solve the exception from the title.

What I tried so far:

  • I've attempted to remove the " as no tracking" and keep a tracked instance in the static List. This lead to EF claiming Character ID already exists as a tracked entity.
  • Based on the above i tried to use this in my update method in character repository but it leads to exactly the same issue
_db.Entry(playableCharacter).State = EntityState.Detached;
_db.Set<PlayableCharacter>().Update(playableCharacter);
await _db.SaveChangesAsync();
  • I've made sure all my async calls to DB are awaited to make sure there is no issue from this side.

  • I've added .AsNoTracking to the request retrieving the playable character before adding it to the static list to make sure EF doesn't have any issues from this perspective.

  • I've tried using the _context.Entry(PlayableCharacter) methods to force the modified entity state. It worked. That's what worked the best so far. But it forced me to iterate on both many to many lists and I would like to avoid that cause it feels like hacking EF instead of understanding it and working with it.

  • I've made sure the context change tracker is empty when leaving the .AsNonTracking request for the game instance in the controller. Meaning this might be somehow related to that static list but since the instance in the static list is also non tracking how can that be the issue? If that was the case, using .AsNoTracking() on the request in the controller should be enough for it to work.

  • I've tried many different approaches but it has been 8 hours now, the last 3 attempts led me to a successful update on all linked entities [many to many collections removal excluded] (while having only _context.PlayableCharacters.Update(playableCharacter) in the repository) and that's the version I posted. But clicking the" browser link" function in visual studio led me to square 1, now EF has issues with PlayableCharacters.LinkedItems.

And this means I completely misunderstood the way EF treats his context instances.

Any help in understanding how exactly EF works with entities in static lists hopefully getting the update method to work in a reliable way instead of what seems to be now complete randomness is appreciated.

Edit: Checking the _context.ChangeTracker.Longview prior to using the update method shows there is no tracked object at all(And that's expected).

Still using the update method returns the same exception which means EF is actually tracking this object somehow without telling me where and why.

I expect him to not be tracking anything at all when this update function is called.

CodePudding user response:

Problem is finally solved.

  1. It turns out while requesting one of my objects from EF context I used .Include(x=>x.User) twice. While I heard EF would ignore a duplicated Include it doesn't seem to be the case.

All many to many relations for this user were duplicated( ID included).

Meaning the entity with a given ID was indeed already tracked despite being unique in the DB and everywhere else in the code.

  1. The usage of a static List.

While this seemed smart at first( to limit the amount of requests to DB ) it clearly caused more issues than I thought forcing me to " map" those changes back instead of applying them with one method.

At this point it will be faster to get rid of the static list.

  • Related