Update Entity from ViewModel in MVC using AutoMapper
The cause
The line ...
Mapper.Map(supplier, updatedSupplier);
... does a lot more than meets the eye.
- During the mapping operation,
updatedSupplier
loads its collections (Addresses
, etc) lazily because AutoMapper (AM) accesses them. You can verify this by monitoring SQL statements. - AM replaces these loaded collections by the collections it maps from the view model. This happens despite the
UseDestinationValue
setting. (Personally, I think this setting is incomprehensible.)
This replacement has some unexpected consequences:
- It leaves the original items in the collections attached to the context, but no longer in scope of the method you're in. The items are still in the
Local
collections (likecontext.Addresses.Local
) but now deprived of their parent, because EF has executed relationship fixup. Their state isModified
. - It attaches the items from the view model to the context in an
Added
state. After all, they're new to the context. If at this point you'd expect 1Address
incontext.Addresses.Local
, you'd see 2. But you only see the added items in the debugger.
It's these parent-less 'Modified` items that cause the exception. And if it didn't, the next surprise would have been that you add new items to the database while you only expected updates.
OK, now what?
So how do you fix this?
A. I tried to replay your scenario as closely as possible. For me, one possible fix consisted of two modifications:
Disable lazy loading. I don't know how you would arrange this with your repositories, but somewhere there should be a line like
context.Configuration.LazyLoadingEnabled = false;
Doing this, you'll only have the
Added
items, not the hiddenModified
items.Mark the
Added
items asModified
. Again, "somewhere", put lines likeforeach (var addr in updatedSupplier.Addresses) { context.Entry(addr).State = System.Data.Entity.EntityState.Modified; }
... and so on.
B. Another option is to map the view model to new entity objects ...
var updatedSupplier = Mapper.Map<Supplier>(supplier);
... and mark it, and all of its children, as Modified
. This is quite "expensive" in terms of updates though, see the next point.
C. A better fix in my opinion is to take AM out of the equation completely and paint the state manually. I'm always wary of using AM for complex mapping scenarios. First, because the mapping itself is defined a long way away from the code where it's used, making code difficult to inspect. But mainly because it brings its own ways of doing things. It's not always clear how it interacts with other delicate operations --like change tracking.
Painting the state is a painstaking procedure. The basis could be a statement like ...
context.Entry(updatedSupplier).CurrentValues.SetValues(supplier);
... which copies supplier
's scalar properties to updatedSupplier
if their names match. Or you could use AM (after all) to map individual view models to their entity counterparts, but ignoring the navigation properties.
Option C gives you fine-grained control over what gets updated, as you originally intended, instead of the sweeping update of option B. When in doubt, this may help you decide which option to use.
I searched all stackoverflow answers and google searches. Finally i just added 'db.Configuration.LazyLoadingEnabled = false;' line and it worked perfectly for me.
var message = JsonConvert.DeserializeObject<UserMessage>(@"{.....}"); using (var db = new OracleDbContex()) { db.Configuration.LazyLoadingEnabled = false; var msguser = Mapper.Map<BAPUSER>(message); var dbuser = db.BAPUSER.FirstOrDefault(w => w.BAPUSERID == 1111); Mapper.Map(msguser, dbuser); // db.Entry(userx).State = EntityState.Modified; db.SaveChanges(); }