Home > Net >  Entity Framework: Entity of type x cannot be tracked because another instance with same key is alrea
Entity Framework: Entity of type x cannot be tracked because another instance with same key is alrea

Time:11-29

When I try to update a Tafel record, I receive an error. Restaurant has a navigational collection of Tafels. I'm not quite sure if I should add the restaurant as a parameter but a Tafel has a composite primary key and restaurantId is part of it (together with a tablenumber)... I'm quite new to EF.. I check for possible nulls at first, then if they both exist and whether the 'tafel' record has any changes to it.

public void UpdateTafel(Tafel tafel, Restaurant restaurant)
    {
        if (tafel == null) throw new RestaurantManagerException("UpdateTafel: Tafel mag niet null zijn");
        if (restaurant == null) throw new RestaurantManagerException("UpdateTafel: Restaurant mag niet null zijn");
        try
        {
            if (!_restaurantRepository.BestaatRestaurant(restaurant.Id)) throw new RestaurantManagerException("GeefAlleTafelsVanRestaurant - restaurant bestaat niet");
            if (!_restaurantRepository.BestaatTafel(tafel.Tafelnummer, restaurant)) throw new RestaurantManagerException("GeefAlleTafelsVanRestaurant - tafel bestaat niet");
            Tafel db = _restaurantRepository.GeefTafel(tafel.Tafelnummer, restaurant);
            if (tafel.IsDezelfde(db)) throw new ReservatieManagerException("Niks gewijzigd");
            _restaurantRepository.UpdateTafel(tafel, restaurant);
        }
        catch (Exception ex)
        {
            throw new ReservatieManagerException("UpdateTafel", ex);
        }
    }

However when he gets to the repository method where EF has to actually update the said Tafel record with following method:

public void UpdateTafel(Tafel tafel, Restaurant restaurant)
        {
            _context.Tafels.Update(tafel);
            _context.SaveChanges();
        }

I receive an exception stating that another instance of Restaurant with the same key is already being tracked allthough I don't see quite how/where this happens...


Inner Exception 1:
InvalidOperationException: The instance of entity type 'Restaurant' cannot be tracked because another instance with the key value '{Id: 1}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.

public void UpdateTafel(Tafel tafel, Restaurant restaurant)
        {
            _context.Entry(restaurant).State = EntityState.Detached;
            _context.Tafels.Update(tafel);
            _context.SaveChanges();
        }

CodePudding user response:

If the tafel is a result of .Find method, it is really already tracked. So, the .Update tries to start it's tracking for the second time and throws the error. https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.update

CodePudding user response:

This error occurs when you start mixing different entity instances referencing the same data. This commonly happens when deserializing data into entities, or if your DbContext reference lifetime scope is set to Transient where different repositories etc. end up loading and tracking entities, then those individual instances start getting passed around.

The culprit in your case is probably this method:

if (!_restaurantRepository.BestaatRestaurant(restaurant.Id)) throw new RestaurantManagerException("GeefAlleTafelsVanRestaurant - restaurant bestaat niet");

"restaurant" as a reference to a instance of a restaurant record in the scope of the DbContext that the repository is using, isn't tracked. This class instance was either loaded by a different DbContext instance or constructed from data. My guess is that the above call is telling the Repository to Load a restaurant by ID and within the scope of that method will be something like:

var restaurant = _context.Restaurants.SingleOrDefault(r => r.Id == restaurantId);

followed maybe by some more logic, a check if restaurant is null, etc. before returning True or False.

The issue is that even if you don't do anything with or return that reference you've loaded, the DbContext instance that the repository is using is now tracking that entity for that restaurant.

So if you do something like make a new Tafel and assign it the restaurant reference from "outside" the scope, that instance the DbContext isn't tracking, then try to save that Tafel, you get the exception that the DbContext is already tracking an entity with the same ID. (The reference that was loaded by BestaatRestaurant and/or other calls that might have fetched an entity.)

The obvious fix might sound like using AsNoTracking() in calls that read data to avoid keeping tracking references, and this would prevent errors like this, however it's very easy to forget to do, leading to situational exceptions, and you will probably find that even without this exception you'd probably find that using Update on your Tafel with the untracked Restaurant reference would have some undesireable behaviour like throwing an exception about a duplicate unique constraint on the PK, or it would duplicate the Restaurant in the DB with a new ID.

Working with untracked / detached entities is tricky and honestly should be avoided. If your goal is to save a Tafel and associate it to an existing Restaurant, then AVOID passing entity references around and instead just pass the ID of the entity to associate. For instance:

public void UpdateTafel(TafelViewModel tafel, int restaurantId)
{
    if (tafel == null) throw new RestaurantManagerException("UpdateTafel: Tafel mag niet null zijn");

    try
    {
        var restaurant = _restaurantRepository.BestaatRestaurant(restaurantId); // Fetch the restaurant and return it after validating, Throw if not found/valid.

        if (!_restaurantRepository.BestaatTafel(tafel.Tafelnummer)) 
            throw new RestaurantManagerException("GeefAlleTafelsVanRestaurant - tafel bestaat niet");
        Tafel existingTafel = _restaurantRepository.GeefTafel(tafel.Tafelnummer); // Ensure this eager loads Tafel.Restaurant
        // If the Restaurant has changed then update the reference in our tracked entities.
        if (existingTafel.Restaurant.Id != restaurant.Id)
            existingTafel.Restaurant = restaurant; 
        // I'm assuming this is doing something like updating the database instance with the values in the passed in reference?  If so you could pass the Restaurant instance above in and do that check/update here.
        if (tafel.IsDezelfde(existingTafel)) throw new ReservatieManagerException("Niks gewijzigd");
        _restaurantRepository.SaveChanges();
    }
    catch (Exception ex)
    {
        throw new ReservatieManagerException("UpdateTafel", ex);
    }
}

The key differences I would outline. First, note that the model passed into this method is not a Tafel entity, but rather a view model. You can use an entity, but you should avoid confusing data that doesn't actually represent a tracked, whole entity with a proper entity. Methods that accept an entity shouldn't need to make assumptions or checks whether this is just data filled in or deserialized vs. a living breathing tracked entity reference. We also just pass the selected restaurant ID. The verification for the restaurant actually loads and returns a tracked Restaurant reference that we can associate. We also go to the repository to retrieve the tracked, live existing Tafel instance based on the ID of the view model passed in. From there we can copy values from the view model into the tracked entity, including associating a new tracked Restaurant reference if it's changed. From there since we are dealing with tracked entities, the only thing left to do is tell the DbContext (through the repository) to SaveChanges. There is no need to use Update. There should be only one tracked instance of each entity being within the scope of the DbContext. Update is used for untracked, detached entities.

  • Related