Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

The instance of the entity type cannot be tracked because another instance with the keyvalue is being tracked

Tags:

I am basically trying to implement CRUD using EntityFrameWork core and .Net core 3.1. I have an issue with my update operation where I am not able update the context with the modified value. I am using postman to initiate the request.

As you can see in the code below, I am trying to check if that customer exist and if it does pass the modified object to the context.

enter image description here

Function code

   [FunctionName("EditCustomer")]
    public async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Anonymous,"post", Route = "update-customer")] HttpRequest req)
    {
        var customer = JsonConvert.DeserializeObject<CustomerViewModel>(new StreamReader(req.Body).ReadToEnd());
        await _repo.UpdateCustomer(customer);
        return new OkResult();
    }

Repository method

  public async Task UpdateCustomer(CustomerViewModel customerViewModel)
    {
        if (customerViewModel.CustomerId != null)
        {
            var customer = _context.Customers.Where(c => c.CustomerId.Equals(customerViewModel.CustomerId)).FirstOrDefault();

            if (customer == null)
            {
                throw new Exception("customer not found");
            }
            else
            {
                _context.Customers.Update(_mapper.Map<Customers>(customerViewModel));
                await _context.SaveChangesAsync();

            }
        }
       
    }

Mapping

   public class CustomerManagerProfile : Profile
    {
        public CustomerManagerProfile()
        {
            CreateMap<CustomerDetails, CustomerDetailsViewModel>().ReverseMap();
            CreateMap<CustomerOrders, CustomerOrdersViewModel>().ReverseMap();
            CreateMap<CustomerOrderDetails, OrderDetailsViewModel>().ReverseMap();
            CreateMap<Customers, CustomerViewModel>().ReverseMap();
        }
    }

Solution

public async Task UpdateCustomer(CustomerViewModel customerViewModel)
    {
        if (customerViewModel.CustomerId != null)
        {
            var customer = _context.Customers.Where(c => c.CustomerId.Equals(customerViewModel.CustomerId)).FirstOrDefault();

            if (customer == null)
            {
                throw new Exception("customer not found");
            }
            else
            {
                var customerModel = _mapper.Map<Customers>(customerViewModel);
                _context.Entry<Customers>(customer).State = EntityState.Detached;
                _context.Entry<Customers>(customerModel).State = EntityState.Modified;
                await _context.SaveChangesAsync();

            }
        }
}
  

     
like image 684
Tom Avatar asked Jul 16 '20 11:07

Tom


2 Answers

Entity Framework tracks your entities for you. For simplicity's sake, think of it like keeping a dictionary (for every table) where the dictionary key is equal to your entity's PK.

The issue is that you can't add two items of the same key in a dictionary, and the same logic applies to EF's change tracker.

Let's look at your repository:

var customer = _context
                  .Customers
                  .Where(c => c.CustomerId.Equals(customerViewModel.CustomerId))
                  .FirstOrDefault();

The fetched customer is retrieved from the database and the change tracker puts it in his dictionary.

var mappedCustomer = _mapper.Map<Customers>(customerViewModel);
_context.Customers.Update();

I split your code in two steps for the sake of my explanation.

It's important to realize that EF can only save changes to tracked objects. So when you call Update, EF executes the following check:

  • Is this the same (reference-equal) object as one I have I my change tracker?
  • If yes, then it's already in my change tracker.
  • If not, then add this object to my change tracker.

In your case, the mappedCustomer is a different object than customer, and therefore EF tries to add mappedCustomer to the change tracker. Since customer is already in there, and customer and mappedCustomer have the same PK value, this creates a conflict.

The exception you see is the outcome of that conflict.

Since you don't need to actually track your original customer object (since EF doesn't do anything with it after fetching it), the shortest solution is to tell EF to not track customer:

var customer = _context
                  .Customers
                  .AsNoTracking()
                  .Where(c => c.CustomerId.Equals(customerViewModel.CustomerId))
                  .FirstOrDefault();

Since customer is now not put into the change tracker, mappedCustomer won't cause a conflict anymore.

However, you don't actually need to fetch this customer at all. You're only interested in knowing whether it exists. So instead of letting EF fetch the entire customer object, we can do this:

bool customerExists = _context
                        .Customers
                        .Any(c => c.CustomerId.Equals(customerViewModel.CustomerId));

This also solves the issue since you never fetch the original customer, so it never gets tracked. It also saves you a bit of bandwidth in the process. It's admittedly negligible by itself, but if you repeat this improvement across your codebase, it may become more significent.

like image 53
Flater Avatar answered Sep 30 '22 01:09

Flater


The most simple adjustment that you could make would be to avoid tracking your Customers on retrieval like this:

var customer = _context
    .Customers
    .AsNoTracking() // This method tells EF not to track results of the query.
    .Where(c => c.CustomerId.Equals(customerViewModel.CustomerId))
    .FirstOrDefault();

It's not entirely clear from the code, but my guess is your mapper returns a new instance of Customer with the same ID, which confuses EF. If you would instead modify that same instance, your call to .Update() should work as well:

var customer = _context.Customers.Where(c => c.CustomerId.Equals(customerViewModel.CustomerId)).FirstOrDefault();
customer.Name = "UpdatedName"; // An example.
_context.Customers.Update(customer);
await _context.SaveChangesAsync();

As a matter of fact, if you track your Customer you don't even need to explicitly call .Update() method, the purpose of tracking is to be aware of what changes were made to the entities and should be saved to the database. Therefore this will also work:

// Customer is being tracked by default.
var customer = _context.Customers.Where(c => c.CustomerId.Equals(customerViewModel.CustomerId)).FirstOrDefault();
customer.Name = "UpdatedName"; // An example.
await _context.SaveChangesAsync();

EDIT:
The solution you yourself provide begins by tracking the results of your query (the Customer) instance, then stops tracking it (a.k.a. gets detached) before writing to database and instead starts tracking the instance that represents the updated Customer and also marks it as modified. Obviously that works as well, but is just a less efficient and elegant way of doing so.
As a matter of fact if you use this bizarre approach, I don't see the reason for fetching your Customer at all. Surely you could just:

if (!(await _context.Customers.AnyAsync(c => c.CustomerId == customerViewModel.CustomerId)))
{
    throw new Exception("customer not found");
}

var customerModel = _mapper.Map<Customers>(customerViewModel);
_context.Customers.Update(customerModel);
await _context.SaveChangesAsync();
like image 28
Marchyello Avatar answered Sep 30 '22 01:09

Marchyello