实体框架核心:无法更新具有嵌套值对象的实体

15

我有一个实体,它有一个值对象,这个值对象又有另一个值对象。我的问题是,在更新实体以及其值对象时,具有父值对象的实体得到更新,但子值对象没有被更新。

请注意,我使用了Entity Framework Core的最新版本2.1.0-rc1-final

这是父实体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; }
}

这是父值对象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; }
}

这是子值对象 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; }
}

在更新员工信息时,我首先从数据库中获取员工信息,然后使用从用户界面获取的新值更改地址属性。

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

还有SetAddress方法:

public void SetAddress(Address address)
{
    Guard.AssertArgumentNotNull(address, nameof(address));
    Address = address;
}

你的实体更新代码在哪里? - Vivek Nuna
嗨@vivek,如上所述,我只是从数据库中获取实体,然后使用员工类中的SetAddress方法设置地址。 - yo2011
是的,因此发布SetAddress方法的实现。 - Vivek Nuna
我更新了帖子。 - yo2011
你应该使用地址存储库而不是员工来更新地址。 - Vivek Nuna
显示剩余2条评论
2个回答

26
根据这个 EF Core GitHub ticket,你需要直接更新子/嵌套/拥有类型的属性,才能使其正确跟踪。这在 EF 2.1 中应该得到修复(目前仅作为发布候选版),但可能没有被包含在内。在 2.0.3 中,他们更新了异常的措辞为:

InvalidOperationException:实体类型“Parent.Child#Child”的实例无法被跟踪,因为已经存在具有相同键值“{ 'ParentID' }”的另一个实例正在被跟踪。 替换所拥有的实体时,请修改属性而不改变实例,或者首先分离以前的所拥有的实体条目。

如果你使用 DDD,那么这条消息的第二部分会让你感到非常困惑。它告诉你必须直接更新子/嵌套属性的属性,以便 EF 正确地跟踪更改(这将破坏 DDD 值对象的不可变性)。根据GitHub 线程上的评论,这里提供了一种建议的、某种程度上符合 DDD 的解决方法,可以适应你的代码。
public void SetAddress(Address address)
{
    Guard.AssertArgumentNotNull(address, nameof(address));    
    Address.UpdateFrom(address);
}
// And on Address:
internal void UpdateFrom(Address other)
{
    Street = other.Street;
    // ...
}

第二个建议的解决方法是将实体分离,更新Address实例,然后重新附加它。在我的实现中,我没有太多的运气使用这个解决方法,但为了纪念而发布它。也许你比我幸运。
context.Entry(employee.Address).State = EntityState.Detached;
employee.SetAddress(newAddress);
context.Entry(employee.Address).State = EntityState.Modified;

更新

我终于找到了与EF Core团队一起跟踪此问题的开放票证。 Ticket #10551 具体说明了手头的问题,目前仍然未解决。它肯定没有出现在EF Core 2.1中,并且似乎已经被放置在Backlog Milestone 3.0中。请注意,您可以通过投票来引起EF Core团队对此问题的更多关注。

更新2 EF Core 2.2引入了一个Tracked Graph组件,使这个过程变得更加流畅。但是,这需要您所有的EF实体都使用数据库生成的ID。该方法检查实体键是否设置,然后将实体标记为修改或添加。这可以扩展到包括删除,但出于我的目的,我不希望出现这种行为。

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
}

然后,在执行context.SaveChanges();之前,使用context.Upsert(<YOUR ENTITY OBJECT>);

谢谢,它运行良好,但我还在等待EF团队的官方解决方案。 - yo2011
4
受第二个选项启发,我在更新实体时使用了以下方法: var existing = _db.Entities.Find(ent.ID); 找到原始实体 _dbContext.Entry(existing).CurrentValues.SetValues(ent); 更新所有基本值 existing.ChangeValueObject(ent.ValueObject); 手动更新值对象。 - Simopaa

-1

@theMisir:是的,它确实可以。 - Jogai

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接