Entity Framework Core: Fail to update Entity with nested value objects
Asked Answered
M

2

15

I have an entity that has a value object and this value object has another value object. My issue is that when updating the entity along with the value objects, the entity with the parent value object get updated but the child value object didn't. note, I used the latest version of Entity Framework Core 2.1.0-rc1-final.

This is the parent entity Employee:

public class Employee : Entity
{
    public string FirstName { get; private set; }
    public string LastName { get; private set; }
    public string Email { get; private set; }
    public Address Address { get; private set; }
}

and this is the parent value object Address:

public class Address : ValueObject<Address>
{
    private Address() { }

    public Address(string street, string city, string state, string country, string zipcode, GeoLocation geoLocation)
    {
        Street = street;
        City = city;
        State = state;
        Country = country;
        ZipCode = zipcode;
        GeoLocation = geoLocation;
    }

    public string Street { get; private set; }
    public string City { get; private set; }
    public string State { get; private set; }
    public string Country { get; private set; }
    public string ZipCode { get; private set; }
    public GeoLocation GeoLocation { get; private set; }
}

and this is the child value object GeoLocation:

public class GeoLocation
{
    private GeoLocation()
    {

    }

    public GeoLocation(decimal longitude, decimal latitude)
    {
        Latitude = latitude;
        Longitude = longitude;
    }
    public Decimal Longitude { get; private set; }
    public Decimal Latitude { get; private set; }
}

and when updating the employee, I first get it from the database, then change the Address property using the new value obtained from the user interface.

var employee = _repository.GetEmployee(empId);
employee.SetAddress(newAddress);

and the SetAddress method:

public void SetAddress(Address address)
{
    Guard.AssertArgumentNotNull(address, nameof(address));
    Address = address;
}
Mccormac answered 23/5, 2018 at 10:58 Comment(7)
Where is your code to update entity?Geophysics
Hi @vivek, as described above, i just get entity from database, then set address using a method in the employee called SetAddressMccormac
Yes so post SetAddress method implementationGeophysics
i updated the postMccormac
You should update Address using address repository not with employeeGeophysics
Address is a value Object and the ValueObject doesn't have a repositoryMccormac
Then you have to ingore the other fields on updateGeophysics
W
26

According to this EF Core GitHub ticket you have to update the child/nested/owned type properties directly for it to track properly. This was supposed to be fixed in EF 2.1 (currently only available as a release candidate) but may have not made the cut. In 2.0.3 they updated the verbiage of the exception to:

InvalidOperationException: The instance of entity type 'Parent.Child#Child' cannot be tracked because another instance with the same key value for {'ParentID'} is already being tracked. When replacing owned entities modify the properties without changing the instance or detach the previous owned entity entry first.

The second part of this message will make you throw up a little if you are using DDD. It is telling you that you must update the properties of the child/nested properties directly for EF to properly track the changes (which breaks DDD Value Objects as being immutable). As per a comment on the GitHub thread here is a suggested, somewhat DDD friendly, workaround adapted to match your code:

public void SetAddress(Address address)
{
    Guard.AssertArgumentNotNull(address, nameof(address));    
    Address.UpdateFrom(address);
}
// And on Address:
internal void UpdateFrom(Address other)
{
    Street = other.Street;
    // ...
}

-OR-

The second suggested workaround is done by detaching the entity, updated the instance of Address, then re-attaching it. I didn't have much luck with this workaround in my implementation, but will post it for posterity. Maybe you'll have better luck with it than I did.

context.Entry(employee.Address).State = EntityState.Detached;
employee.SetAddress(newAddress);
context.Entry(employee.Address).State = EntityState.Modified;

UPDATE

I finally found the open ticket with the EF Core team that can be tracked for this issue. Ticket #10551 specifically states the issue at hand and is still open. It definitely didn't make it to EF Core 2.1 and appears to have been placed in the Backlog Milestone 3.0. Note you can up-vote this issue as a way to get the EF Core team to put more attention on it.

UPDATE 2 EF Core 2.2 introduced a Tracked Graph component that makes this much more fluid. This does, however, require that all of your EF Entities use database generated ids. This method inspects whether the entity key is set, then flags the entity as modified or added. This can be expanded to include deletes, but for my purposes I don't want that sort of behavior.

internal void Upsert(object entity)
{
    ChangeTracker.TrackGraph(entity, e =>
    {
        if (e.Entry.IsKeySet)
        {
            e.Entry.State = EntityState.Modified;
        }
        else
        {
            e.Entry.State = EntityState.Added;
        }
    });

    #if DEBUG
    foreach (var entry in ChangeTracker.Entries())
    {
        Debug.WriteLine($"Entity: {entry.Entity.GetType().Name} State: {entry.State.ToString()}");
    }
    #endif
}

Then, use the context.Upsert(<YOUR ENTITY OBJECT>); before context.SaveChanges();.

Willingham answered 23/5, 2018 at 12:18 Comment(2)
Thanks, it works fine but waiting for the official solution from EF TeamMccormac
Inspired by the second option, this worked for me when updating an entity: var existing = _db.Entities.Find(ent.ID); Find the original _dbContext.Entry(existing).CurrentValues.SetValues(ent); Update all of the basic values existing.ChangeValueObject(ent.ValueObject); Manually update Value ObjectAbutment
T
-1

A simpler alternative can be found at Owned type property not persisting for a modified entity in EF Core

in short, instead of

_context.Entry(contactModelFromRequestBody).State = EntityState.Modified;

you should use

_context.Update(contactModelFromRequestBody);
Thermostatics answered 9/3, 2020 at 23:52 Comment(1)
@theMisir: yes it doesOrthopedics

© 2022 - 2024 — McMap. All rights reserved.