3

Models:

 public class Client{
    public int Id {get;set;}

    public string Name {get;set;}

    public Address Address {get;set;}

    public int AddressId  {get;set;}
}

public class Address{
   public int Id

   public string Address1 {get;set;}

   public string PostCode {get;set;}
}

View Models:

public class ClientViewNodel{
        public int Id {get;set;}

        public string Name {get;set;}

        public Address Address {get;set;}

        public int AddressId  {get;set;}
    }

    public class AddressViewModel{
       public int Id

       public string Address1 {get;set;}

       public string PostCode {get;set;}
    }

Mapping:

 Mapper.Initialize(config =>
    {
        config.CreateMap<ClientViewModel, Client>().ReverseMap();
        config.CreateMap<AddressViewModel, Address>().ReverseMap();
    });

Controller Update action:

[HttpPost]
public async Task<IActionResult> Update(cLIENTViewModel viewModel)
{
    if (!ModelState.IsValid)
    {
        return View("Client",viewModel);
    }

    var client= _clientRepository.GetClient(viewModel.Id);
    if (client == null) 
        return NotFound();

    client= Mapper.Map<ClientViewModel, Client>(viewModel);

    _clientRepository.Update(client);
    var result = await _clientRepository.SaveChangesAsync();

    if (result.Success)
    {
        return RedirectToAction("Index");
    }

    ModelState.AddModelError("", result.Message);

    return View("Client",viewModel);
}

The problem is that when _clientRepository.Update(client) is called I get an error message saying that:

The instance of entity type 'Client' cannot be tracked because another instance of this type with the same key is already being tracked. When adding new entities, for most key types a unique temporary key value will be created if no key is set (i.e. if the key property is assigned the default value for its type). If you are explicitly setting key values for new entities, ensure they do not collide with existing entities or temporary values generated for other new entities. When attaching existing entities, ensure that only one entity instance with a given key value is attached to the context.

When I debugged the code I can see that when I map the viewModel to model The AddressID in client model is set to 0. I am guessing that this is causing the problem.

How can I map the viewModel back to model where the details of address will be updated such as Address1 and Postcode instead of the Id.

I have also tried to ignore mapping for Id for Address in the mapping with .ForMember(x => x.AddressId, opt => opt.Ignore())

But still it sets the AddressId to 0.

What am I missing?

kayess
  • 3,384
  • 9
  • 28
  • 45
akd
  • 6,538
  • 16
  • 70
  • 112
  • Sorry to be direct, but you are using AutoMapper in the completely wrong way! AutoMapper was **NEVER** intended for two-way mapping and it's author, Jimmy Bogard, never had a plausible use case for it. And when you look close at it, there is no reason for 2 way mapping except for laziness. Feel free to read this post here http://lostechies.com/jimmybogard/2009/09/18/the-case-for-two-way-mapping-in-automapper/ from Jimmy where he explains why and the use cases it was intended for. It is always from domain or persistence model to a dto or ViewModel! – Tseng Dec 05 '16 at 20:30
  • So when you get a post back from your MVC action or WebAPI call, just do it as you did before you had AutoMapper. Use the ID, fetch the Model from the database, update it's fields, call SaveChanges() (or not, if something happens and you want to roll it back). You will need to validate your data anyways and depending on your logic certain fields do not carry over under certain conditions (i.e. don't add "user greetings" to an order if its not wrapped ordered as "wrap as gift" – Tseng Dec 05 '16 at 20:33
  • This also may be useful for you: http://www.devtrends.co.uk/blog/stop-using-automapper-in-your-data-access-code – Tseng Dec 05 '16 at 20:35
  • When I watch plural-sight tutorials they all use it to do 2 way mapping. I can see the danger of doing this. But they also say that an action must be max 10 lines. So tell me how would I keep the action 10 lines without using automapper? When I need to map over 10 properties back to model? I shoudl pass the viewmodel back to service and do the mapping in the service? – akd Dec 05 '16 at 20:36
  • First its a guideline,not a hard rule.And if they use AutoMapper in these tutorials with 2-way binding they didn't understand the tool either. You gonna question the author of the tool and an expert in DDD? :P Second I think these people confuse lines of code with instructions.When you put each property assignment in a new row its still one "instruction" just split over multiple lines.Also you may want to read this http://rogerjohansson.blog/2013/12/01/why-mapping-dtos-to-entities-using-automapper-and-entityframework-is-horrible/ on the topic to safe yourself from the horrors with this attempt – Tseng Dec 05 '16 at 20:43

1 Answers1

3

When you do Mapper.Map<ClientViewModel, Client>(viewModel), AutoMapper creates a new Client object, with the same ID as the existing Client object.

You then instruct EntityFramework to Update this object graph. Entity Framework is not tracking your new Client object, so it attaches the object to its internal magic cache/tracking sauce. This fails because of an ID collission. Hence the error "The instance of entity type 'Client' cannot be tracked because another instance of this type with the same key is already being tracked".

This is also the source of the 0 AddressId. The Address object is also a brand new object, created by AutoMapper, and the property has the value default(int), because it's never assigned another value by AutoMapper after it's been created.

gnud
  • 77,584
  • 5
  • 64
  • 78
  • Yes that's right. When I load the model and map each individual property manually then _clientRepository.SaveChangesAsync(); works by itself without using _clientRepository.Update(client);. I am aware that I dont know how to use AutoMapper to map viewModel to model. Can you please give an example how I should map viewModel to model ? – akd Dec 05 '16 at 20:11
  • You can map "onto" an existing object by doing `Mapper.Map(sourceObject, destObject)`. That will work for the top-level Client, but I don't know off-hand if it will work on the children as well. Worth a try, I suppose :) – gnud Dec 05 '16 at 20:17
  • This must be a joke but it works :) `Mapper.Map(viewModel, property);' Thanks. Then I found an article here which explains the differences: [link](http://cpratt.co/using-automapper-mapping-instances/) – akd Dec 05 '16 at 20:31
  • Great! Be careful, though. You _will_ run into trouble if this mapping changes the ID of an object tracked by EntityFramework, or removes a relation between two entities. – gnud Dec 05 '16 at 21:48