简体   繁体   中英

Problem with EF Core updating nested entities when using automapper

I am maintaining an application which uses EF Core to persist data to a SQL database.

I am trying to implement a new feature which requires me to retrieve an object from the database (Lets pretend its an order) manipulate it and some of the order lines which are attached to it and save it back into the database. Which wouldn't be a problem but I have inherited some of this code so need to try to stick to the existing way of doing things.

The basic process for data access is :

UI -> API -> Service -> Repository -> DataContext

The methods in the repo follow this pattern (Though I have simplified it for the purposes of this question)

public Order GetOrder(int id)
{
    return _context.Orders.Include(o=>o.OrderLines).FirstOrDefault(x=>x.Id == id);
}

The service is where business logic and mapping to DTOs are applied, this is what the GetOrder method would look like :

public OrderDTO GetOrder(int id)
{
    var ord = _repo.GetOrder(id);

    return _mapper.Map<OrderDto>(ord);
}

So to retrieve and manipulate an order my code would look something like this

public void ManipulateAnOrder()
{
    // Get the order DTO from the service
    var order = _service.GetOrder(3);

    // Manipulate the order
    order.UpdatedBy = "Daneel Olivaw";
    order.OrderLines.ForEach(ol=>ol.UpdatedBy = "Daneel Olivaw");

    _service.SaveOrder(order);
}

And the method in the service which allows this to be saved back to the DB would look something like this:

public void SaveOrder(OrderDTO order)
{
    // Get the original item from the database
    var original = _repo.GetOrder(order.Id);

    // Merge the original and the new DTO together
    _mapper.Map(order, original);

    _repo.Save(original);
}

Finally the repositories save method looks like this

public void Save(Order order){

    _context.Update(order)
    _context.SaveChanges();

}

The problem that I am encountering is using this method of mapping the Entities from the context into DTOs and back again causes the nested objects (in this instance the OrderLines) to be changed (or recreated) by AutoMapper in such a way that EF no longer recognises them as being the entities that it has just given to us.

This results in errors when updating along the lines of

InvalidOperationException the instance of ProductLine cannot be tracked because another instance with the same key value for {'Id'} is already being tracked.

Now to me, its not that there is ANOTHER instance of the object being tracked, its the same one, but I understand that the mapping process has broken that link and EF can no longer determine that they are the same object.

So, I have been looking for ways to rectify this, There are two ways that have jumped out at me as being promising,

  1. the answer mentioned here EF & Automapper. Update nested collections

  2. Automapper.Collection

Automapper.collection seems to be the better route, but I cant find a good working example of it in use, and the implementation that I have done doesn't seem to work.

So, I'm looking for advice from anyone who has either used automapper collections before successfully or anyone that has any suggestions as to how best to approach this.

Edit, I have knocked up a quick console app as an example, Note that when I say quick I mean... Horrible there is no DI or anything like that, I have done away with the repositories and services to keep it simple.

I have also left in a commented out mapper profile which does work, but isn't ideal.. You will see what I mean when you look at it.

Repo is here https://github.com/DavidDBD/AutomapperExample

Ok, after examining every scenario and counting on the fact that i did what you're trying to do in my previous project and it worked out of the box.

Updating your EntityFramework Core nuget packages to the latest stable version (3.1.8) solved the issue without modifying your code.

AutoMapper in fact "has broken that link" and the mapped entities you are trying to save are a set of new objects, not previously tracked by your DbContext . If the mapped entities were the same objects, you wouldn't have get this error.

In fact, it has nothing to do with AutoMapper and the mapping process, but how the DbContext is being used and how the entity states are being managed.

In your ManipulateAnOrder method after getting the mapped entities -

var order = _service.GetOrder(3);

your DbContext instance is still alive and at the repository layer it is tracking the entities you just retrieved, while you are modifying the mapped entities -

order.UpdatedBy = "Daneel Olivaw";
order.OrderLines.ForEach(ol=>ol.UpdatedBy = "Daneel Olivaw");

Then, when you are trying to save the modified entities -

_service.SaveOrder(order);

this mapped entities reach the repository layer and DbContext tries to add them to its tracking list, but finds that it already has entities of same type with same Ids in the list (the previously fetched ones). EF can track only one instance of a specific type with a specific key. Hence, the complaining message.

One way to solve this, is when fetching the Order , tell EF not to track it, like at your repository layer -

public Order GetOrder(int id, bool tracking = true)  // optional parameter
{
    if(!tracking)
    {
        return _context.Orders.Include(o=>o.OrderLines).AsNoTracking().FirstOrDefault(x=>x.Id == id);
    }
    return _context.Orders.Include(o=>o.OrderLines).FirstOrDefault(x=>x.Id == id);
}

(or you can add a separate method for handling NoTracking calls) and then at your Service layer -

var order = _repo.GetOrder(id, false);  // for this operation tracking is false

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM