Search code examples
c#entity-frameworkasp.net-core-mvcautomapperinvalidoperationexception

instance of type cannot be tracked because of another instance with same ID error


I have a .net core 2.1 mvc application with EF Core where I use automapper to match viewmodels with domainmodels. In my edit method I get the error :

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

The several other topics here did not solve my problem.

My Edit method:

 [HttpPost]
 [ValidateAntiForgeryToken]
 public async Task<IActionResult> Edit(int id, TicketViewModel ticketViewModel)
    {
    Ticket ticketToUpdate = await _unitOfWork.Ticket.Get(id); // I implement unit of work 

    // some logic for checks
    // ...

        if (ModelState.IsValid)
        {
            try
            {
                // mapping  Ticket viewmodel to Ticket Domain Model
                ticketViewModel = _mapper.Map<TicketViewModel>(ticketToUpdate);

                // update some properties on viewmodel

                _mapper.Map(ticketViewModel, ticketToUpdate); // doesn't map values.

                _unitOfWork.Ticket.Update(ticketToUpdate); //No longer fails
                await _unitOfWork.Commit();
            }
            catch (DbUpdateConcurrencyException)
            {
                return NotFound();
            }
        return RedirectToAction(nameof(Index));
    }

My mapping:

CreateMap<TicketViewModel, Ticket>()
.ForMember(x => x.ID, x => x.MapFrom(y => y.Ticket.ID))
.ForMember(x => x.Title, x => x.MapFrom(y => y.Ticket.Title))
.ForMember(x => x.Description, x => x.MapFrom(y => y.Ticket.Description))


CreateMap<Ticket, TicketViewModel>()
.ForPath(x => x.Ticket.ID, x => x.MapFrom(y => y.ID))
.ForPath(x => x.Ticket.Title, x => x.MapFrom(y => y.Title))
.ForPath(x => x.Ticket.Description, x => x.MapFrom(y => y.Description))

EDIT: The InvalidOperationException is now solved, but the final mapping doesn't seem to map the values of the viewmodel to the _dbcontext entity.


Solution

  • You are loading the domain item, however you are using the wrong automapper call:

    ticketToUpdate = _mapper.Map<Ticket>(ticketViewModel);
    

    This should be:

    _mapper.Map(ticketViewModel, ticketToUpdate);
    

    The first method takes the values from the view model and loads them into a brand new instance of the Entity and assigns it to the ticketToUpdate reference previously loaded. When you go to update that reference, the dbContext behind your unit of work is already tracking the entity with the same ID so you get the error. (The updated reference is treated as a new entity)

    The second Map call example copies the values from the ViewModel over into the entity referenced by the ticketToUpdate. The resulting reference is pointing at the original entity which gets the new values, and the DbContext will save those changes.

    ** Edit: A simple test to outline the behaviour difference with the Map call. If the Map(source, destination) call is not copying over the values you expect, check your mappings to ensure the 2-way conversion is correct.

    [Test]
    public void TestCopyOver()
    {
        var config = new MapperConfiguration(cfg =>
        {
            cfg.CreateMap<ClassA, ClassB>()
                .ForMember(x => x.MyName, x => x.MapFrom(y => y.Name))
                .ForMember(x => x.MyOtherName, x => x.MapFrom(y => y.OtherName));
            cfg.CreateMap<ClassB, ClassA>()
                .ForMember(x => x.Name, x => x.MapFrom(y => y.MyName))
                .ForMember(x => x.OtherName, x => x.MapFrom(y => y.MyOtherName));
        });
    
        var mapper = config.CreateMapper();
    
        ClassA newA = new ClassA { Name = "Fred", OtherName = "Astaire" };
        ClassA altReferenceA = newA;
        Assert.AreSame(newA, altReferenceA, "References don't match.");
        var cloneB = mapper.Map<ClassB>(newA);
        cloneB.MyOtherName = "Rogers";
    
        newA = mapper.Map<ClassA>(cloneB);
        Assert.AreEqual("Rogers", newA.OtherName);
        Assert.AreEqual("Astaire", altReferenceA.OtherName); // original object not updated.
        Assert.AreNotSame(newA, altReferenceA); // now point to 2 different objects
    
        //Reset...
    
        newA = new ClassA { Name = "Fred", OtherName = "Astaire" };
        altReferenceA = newA;
        Assert.AreSame(newA, altReferenceA, "References don't match.");
        cloneB = mapper.Map<ClassB>(newA);
        cloneB.MyOtherName = "Rogers";
    
        mapper.Map(cloneB, newA);
        Assert.AreEqual("Rogers", newA.OtherName);
        Assert.AreEqual("Rogers", altReferenceA.OtherName); // Original object updated.
        Assert.AreSame(newA, altReferenceA); // Still point to same reference.
    }
    

    Here the "newA" represents a reference to an entity pulled from a dbContext. We take a 2nd reference to that same entity to compare later. (altReferenceA). If we call newA = mapper.Map<ClassA>(cloneB) it is now a new reference and this results in the exception with EF. EF is tracking the entity still pointed to by altReferenceA. newA is treated as a new, untracked entity.

    In the second pass we reset the variables and using mapper.Map(cloneB, newA) we copy the value from B to A, both references are updated since they still point to the same object. The tracked entity is updated and can be saved. If the values from B do not get written to newA then I would suspect something is wrong with the mapping configuration from B to A. If the values are being updated in the entity but the entity is not persisting the changes, then I would look at what the Commit() method in your unit of work is trying to do.