Reputation: 1562
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
Upvotes: 34
Views: 48161
Reputation: 436
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
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();
}
}
There are two steps we have taken to resolve the issue.
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.
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
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
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
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
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
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
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