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);
}
}
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.