Search code examples
c#asp.net-mvcentity-framework.net-coreentity

Issue with entity tracker while removing an entity


I am building an e-commerce, more exactly the shopping cart and trying to implementing sessions to add/subtract items when I delete themin the cart. I'am currently using ASP.NET Core MVC with EF Core 7.0.10. I did some research about entity tracker, how to properly delete, update items ecc. but despite this i had this problem with entity tracker. This is the following error :

InvalidOperationException: The instance of entity type 'ShoppingCart' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked.

I read about that untracked entities are better cause of the optimization. In this code i've already applied the asNoTracking() method, so they are not tracked. Down below i show the repository code utilized in the action method of the controller.

public IEnumerable<T> GetAll(
            Expression<Func<T, bool>>? filter = null,
            string? includeProperties = null
        )
        {

            IQueryable<T> query = _dbSet;
           
            // Here i apply a X filter
            if (filter != null)
            {
                query = query.Where(filter);
            }

            // Here i can populate properties
            if (!string.IsNullOrEmpty(includeProperties))
            {
                foreach (
                    var property in includeProperties
                    .Split(
                        new char[] { ',' },
                        StringSplitOptions.RemoveEmptyEntries)
                    )
                {
                    query = query.Include(property);
                }
            }
            return query.ToList();
        }

GetAll() method it's identical to the Get() method (with asNoTracking() too, obviously)

And I also read that to delete an entity that is detached from the content i have to first attach the detached entity to the context and then mark the entity as "deleted" : the following code :

_dbSet.Attach(entity).State = EntityState.Deleted;

I already tried this "strategy" on other code, and it works, but not in this situation. and I don't understand the reason. I tried to write all the action with _context and not with the _unitOfWork and it's works. How it's possible? What's the difference using _context and _unitOfWork ?

This is the action method of the ShoppingCart Controller.

public IActionResult Remove(int cartId)
        {

            ShoppingCart? cart = _unitOfWork.ShoppingCartRepo.Get(x => x.Id == cartId);
            if (cart == null) return NotFound("Cart not found in Remove(int cartId)");

            // Before saving i set new value for the session
            HttpContext.Session.SetInt32(
                key: StaticDetails.SessionCart,
                value: _unitOfWork.ShoppingCartRepo.GetAll
                (filter: x => x.ApplicationUserId == cart.ApplicationUserId)
                .Count() - 1
            );

            _unitOfWork.ShoppingCartRepo.Remove(cart);

            _unitOfWork.Save();

            return RedirectToAction(nameof(Index));

        }

This is the implementation of remove() in its repository.


public void Remove(T entity, bool entityStateDeleted = true)
        {
            if (entityStateDeleted)
            {
                _dbSet.Attach(entity).State = EntityState.Deleted;
            }
            else
            {
                _dbSet.Remove(entity);
            }
        }


Solution

  • GetAll() method it's identical to the Get() method (with AsNoTracking() too, obviously)

    The method Get return a object that isn't tracked. If I reformulate the method Remove(int cartId) like :

    IActionResult Remove(int cartId) // For the exemple, cartId is 42
    {
        ShoppingCart? cart = _dbSet.AsNoTracking().FirstOrDefault(x => x.Id == cartId);
        // The shopping cart 42 is loaded in memomy, but not tracked
    
        List<ShoppingCart> userShoppingCarts = _dbSet.Where(x => x.ApplicationUserId == cart.ApplicationUserId).ToList();
        // All user shopping cart are loaded in memory with tracking.
    
        // Then the shoshopping cart 42 is traked, but from a other object than that is referenced by the variable cart
        ShoppingCart cartDuplicated = userShoppingCarts.First(c => c.Id == cart.Id);
        Object.ReferenceEqual(cart, cartDuplicated); // False
    
        int userShoppingCartsCount = userShoppingCarts.ToList();
    
    
        // The error come from :
        _dbSet.Attach(cart)
        // Throw the error :
        // InvalidOperationException: The instance of entity type 'ShoppingCart' cannot be tracked because
        //     another instance with the same key value for {'Id'} is already being tracked.
    
        // Translated :
        // cart cannot be tracked because cartDuplicated with the id 42 is already being tracked.
        ...
    }
    

    A solution is to inverse where is AsNoTracking.

    Move AsNoTracking from Get to GetAll :

    public IEnumerable<T> GetAll(...)
    {
        IQueryable<T> query = _dbSet.AsNoTacking();
        ...
    }
    

    Then you simply remove :

    public void Remove(T entity)
    {
        _dbSet.Remove(entity);
    }
    

    Remark : To count the use shopping cart, all user shopping cart are loaded in memory. It's really inefficient. I recommend you do something like :

    var count = _dbSet.Where(x => x.ApplicationUserId == cart.ApplicationUserId).Count();
    

    So, only count value is loaded in memory. To do this, you can add a Count method in the repository.