0

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

5NRF
  • 401
  • 3
  • 12
  • 1
    Its programmers of stackoverflow, haha. – Gabriel Llorico Oct 09 '20 at 11:08
  • General tip: please include the method names/signatures. It's jarring to have to guess at which method body belongs to which called method in another snippet. – Flater Oct 09 '20 at 11:18
  • @Flater He did include them but used the code blocks in a wrong way and the edit queue is full. – HMZ Oct 09 '20 at 11:20
  • Well spotted, Caught out by my copy/paste cross posting. Makrkdown formatting between Reddit and SOF is slightly different - Thanks HMZ for sorting that for me :) – 5NRF Oct 09 '20 at 11:33
  • @5NRF When you're updating an entity do not call `Update` just modify the properties you want and call `SaveChanges`. `Update` will mark your whole entity as modified while just calling `SaveChanges` will mark the modified properties only. – HMZ Oct 09 '20 at 11:39
  • My understanding is that SaveChanges doesnt change the EntityState it just does the data persistance? Either way, Tried this and got the same result, Im going to Knock up an example app and stick it up on git hub to demonstrate the issue. – 5NRF Oct 09 '20 at 12:24
  • Example added to the original post. – 5NRF Oct 09 '20 at 12:35

2 Answers2

1

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.

HMZ
  • 2,949
  • 1
  • 19
  • 30
  • Sweet, I used 2.2.6 as that is what the real project is using, Ill have to try the update and check that it doesn't have any knock on effect on the actual project, but that would be great if that is all that's needed. What I ended up doing is writing a generic Extension method on the Mapper Resolution Context that merges the items from the DTO into the Entity without replacing them but that's a workaround at best. Ill try to update EF now and see how I go. – 5NRF Oct 09 '20 at 18:03
0

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
atiyar
  • 7,762
  • 6
  • 34
  • 75
  • That explains why we have not seen any issues until now, normally the Context is buried away behind a rest API so there is a disconnect between the retrieval of the entity, its manipulation and the save process to get in back into the database, with the context being disposed between the get and save process. This is the first time that we have been using the Services and Repos directly from an application so are not disposing the context between getting the record and saving it again. I did try AsNoTracking yesterday without luck, Ill try again now, maybe I missed something. – 5NRF Oct 09 '20 at 18:08
  • @5NRF Yes. In disconnected scenario, it's always a fresh instance of `DbContext` with no existing list of tracked entities. – atiyar Oct 09 '20 at 18:25