Marian Simonca
Marian Simonca

Reputation: 1562

EF Core 'another instance is already being tracked'

I have a problem updating an entity in .Net Core 2.2.0 using EF Core 2.2.3.

An error occurred while saving changes. Error details: The instance of entity type 'Asset' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using

This is how the DB Context is registered:

services.AddDbContext(options =>

options.UseSqlServer(Configuration.GetConnectionString("DbConnection")), ServiceLifetime.Scoped);

The Scoped lifetime is set by default but I wrote it to be more easy to understand.

The Anomaly object is got like this:

public IQueryable<Anomaly> GetAll()
    {return _context.Anomalies.Include(a => a.Asset).Include(a => a.Level)
}

public async Task<Anomaly> GetAnomaly(int anomalyId, User user)
{
    var anomaly = await GetAll()
        .FirstOrDefaultAsync(a => a.Id == anomalyId);

    return anomaly;
}

And the Update() method looks like this:

using (var transaction = _context.Database.BeginTransaction())
{
    try
    {
        _context.Anomalies.Update(anomaly);
        _context.SaveChanges();

        transaction.Commit();
    }
    catch (Exception ex)
    {
        transaction.Rollback();
        throw;
    }
}

It contains some checks before this transaction, but none relevant enough in this context.

This is where I get the error with instance already being tracked. I can't understand how this happens .. If the context is Scoped, then

... "a new instance of the service will be created for each scope", in this case, for each request

If my context on the PUT request is different from the context of the GET request, how is the entity already being tracked? How does this work at the most basic levels?

The only way to make it work is to set the state for all entries from the ChangeTracker to EntityState.Detached. Then it works.. but it makes no sense, at least to my current knowledge..

I found this question but with no valid answer, only with workarounds and assumptions about how EF does the tracking.


UPDATE Here is a link to bitbucket with a sample recreating this problem: EF Core Update Sample

I serialized the objects retrieved from the context.

With Tracking on the LEFT <====> With NO tracking on the RIGHT With Tracking on the LEFT <====> With NO tracking on the RIGHT

Upvotes: 34

Views: 48161

Answers (7)

In my case the problem arose in a situation of batch inserting a list of DTO's into the database, where some of these objects had the same id PK.

To give an example (non-working code):

[HttpPost]
[Route("insert")]
public async Task<ActionResult> Insert([FromBody] List<EventDto> events)
{
    try
    {
        await Task.Run(() =>
        {
            unitOfWork.EventsRepository.InsertRange(events);
            unitOfWork.EventsRepository.SaveChanges();
        });
    }
    catch (Exception e)
    {
        return Unauthorized(e.ToString());
    }

    return Ok();
}

Note I: EventDto is mapped to a table named Events and the object has a primary key named CommandEventId.

Note II: SaveChanges is clearing EF's ChangeTracker. This means that subsequent insertions would not lead to this behavior / error.

That being said, the problem with the above code was that in case there are objects in the list with the same CommandEventId (which is the primary key), EF throws the:

An error occurred while saving changes. Error details: The instance of entity type 'EventDto' cannot be tracked because another instance with the same key value for {'CommandEventId'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using

Concrete Implementation of InsertRange and SaveChanges in UnitOfWork

For the sake of keeping my answer consistent, under the hood, the InsertRange and SaveChanges functions of unitOfWork are not doing anything special (i.e., they are just invoking the corresponding EF functions).

Note that the SaveChanges that UnitOfWork is invoking is clearing EF's ChangeTracker. This means that on subsequent insertions of the same ID's, the error would be different - i.e., PK already exists.

This is the concrete implementation of the functions:

        public void InsertRange(IEnumerable<TEntity> entities)
        {
            dbSet.AddRange(entities);
            if (useCache)
            {
                cacheService.ClearCache(cache);
            }
        }

        public void SaveChanges()
        {
            try
            {
                context.SaveChanges();
            }
            catch (Exception e)
            {

            }
            finally
            {
                context.ChangeTracker.Clear();
            }
        }

Solution

There are two steps we have taken to resolve the issue.

  1. First and foremost, if you end up in a situation where you have duplicate unique identifiers this should be resolved on the client side. In our case, the failed occurrences where of corrupted data so we took the appropriate steps on that.

  2. As far as the API is concerned, even if duplicate data are passed this should not mean that the entire batch insertion should fail.

Implementing a distinct list of unique items (by removing the duplicate ID's) resolved the issue:

[HttpPost]
[Route("insert")]
public async Task<ActionResult> Insert([FromBody] List<EventDto> events)
{
    try
    {
        await Task.Run(() =>
        {

            // There are cases where the data soruce contains duplicate entries, which produce the same hash id.
            // If a duplicate entry is about to be inserted, the whole batch of events is dropped from insertion.
            // For this reason, before inserting the duplicates are removed from the list.
            var distinctCommandEvents = events
                       .GroupBy(x => x.CommandEventId)
                       .Select(g => g.First())
                       .ToList();
            unitOfWork.EventsRepository.InsertRange(distinctCommandEvents);
            unitOfWork.EventsRepository.SaveChanges();
        });
    }
    catch (Exception e)
    {
        return Unauthorized(e.ToString());
    }

    return Ok();
}

Upvotes: 1

Wayne Davis
Wayne Davis

Reputation: 424

I know that I am extremely late to this discussion but I was having the same issue where I could create an entity without issue, but when it came to updating that entity it would succeeded the first time but fail every time thereafter. So I tried all of the suggestions above to no avail. I did some digging around in the docs and found that once I called DbContext.SaveChanges() there was another method that I could call DbContext.ChangeTracker.Clear(). This method is supposed to clear the change tracker of all tracked entities so you can proceed with your future updates. Hopefully, this finds anyone that is having the same frustration with updating entities.

ChangeTracker.Clear() Documentation

Upvotes: 9

Alamakanambra
Alamakanambra

Reputation: 7826

The issue in my case comes from the fact that the entity in a class definition was missing when updating list in 1:many relation.

public class Exercise
{
  public Guid Id { get; set; } 
  //other props...
  public List<EntityInExercise> Entities { get; set; } = new();
}

public class EntityInExercise
{
  public Guid Id { get; set; } 
  //other props...

  //๐Ÿ‘‡this line was missing ๐Ÿ‘‡
  public Exercise Exercise{ get; set; }= null!;
  //โ˜๏ธ
}

In both cases (with and without the missing line) it creates the same db. The only problem I had was when trying to update that list with Automapper and its Collections.

Upvotes: -1

Haktan Enes Bi&#231;er
Haktan Enes Bi&#231;er

Reputation: 682

I was trying to update the same data without noticing. It took me ten hours to realize that. If you have duplicate values like mine,i suggest remove that data... The person who reading this answer may have tried all the solutions on the internet like me, ruined the project, stuck the same error, and omitted duplicate data just like me. You are not alone my friend.

model.GroupBy(gb => gb.ID).Select(s=>s.First()).ToList();//remove duplicates!!!!!

Upvotes: 8

Javier Alvarez
Javier Alvarez

Reputation: 581

I had the same issue but trying to add a second row to the same Entiy. In my case it was because I was assuming the primary key was an Int Identity and auto-generated. As I wasn't assigning any value to it, the second row had the same Id, which is cero default. Lesson learned, when you see this error during an Add() operation in EF, make sure your key is IDENTITY if that's your intention.

Upvotes: 1

Marian Simonca
Marian Simonca

Reputation: 1562

So, in the end we ended up using a custom UpdateEntity method to save our changes on some entities. This method goes through each property, navigation property and collection property of an entity, makes sure there is no chain and updates the objects a single time.

It works for big objects and we only use it in those cases. For simple operations we keep using the simple Update

Here is the link to a bitbucket repository with the source code

In order to use this method, you will have to get the db entity first. Then call UpdateEntity method with the db entity and your entity received by the request.

I hope it helps you as it helped me. Cheers!

Upvotes: 0

Joe Audette
Joe Audette

Reputation: 36696

By default when you retrieve entities they are tracked and since they are tracked you could just call SaveChanges and not call Update. You can also retrieve entities without tracking them by using .AsNoTracking()

calling Update is needed if not tracked already, so if you use AsNoTracking then you do need to use Update before SaveChanges

public IQueryable<Anomaly> GetAll()
{    return _context.Anomalies
    .Include(a => a.Asset)
    .Include(a => a.Level);
}

public async Task<Anomaly> GetAnomaly(int anomalyId, User user)
{
    var anomaly = await GetAll()
        .AsNoTracking()
        .FirstOrDefaultAsync(a => a.Id == anomalyId);

    return anomaly;
}

You can also check if the entity is tracked to know whether to call Update or not:

using (var transaction = _context.Database.BeginTransaction())
{
    try
    {

        bool tracking = _context.ChangeTracker.Entries<Anomaly>().Any(x => x.Entity.Id == anomaly.Id);
        if (!tracking)
        {
            _context.Anomalies.Update(anomaly);
        }

        _context.SaveChanges();

        transaction.Commit();
    }
    catch (Exception ex)
    {
        transaction.Rollback();
        throw;
    }
}

Upvotes: 31

Related Questions