实体框架核心:无法使用嵌套值对象更新实体

yo2*_*011 5 c# value-objects entity-framework-core entity-framework-core-2.1

我有一个具有值对象的实体,该值对象具有另一个值对象.我的问题是,在更新实体和值对象时,具有父值对象的实体会更新,但子值对象不会更新.请注意,我使用的是最新版本的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; }
}
Run Code Online (Sandbox Code Playgroud)

这是父值对象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; }
}
Run Code Online (Sandbox Code Playgroud)

这是子值对象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; }
}
Run Code Online (Sandbox Code Playgroud)

并且在更新员工时,我首先从数据库获取它,然后使用从用户界面获得的新值更改Address属性

var employee = _repository.GetEmployee(empId);
employee.SetAddress(newAddress);
Run Code Online (Sandbox Code Playgroud)

和SetAddress方法

public void SetAddress(Address address)
{
    Guard.AssertArgumentNotNull(address, nameof(address));
    Address = address;
}
Run Code Online (Sandbox Code Playgroud)

bma*_*716 12

根据此EF Core GitHub票证,您必须直接更新子/嵌套/拥有类型属性才能正确跟踪它.这应该在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;
    // ...
}
Run Code Online (Sandbox Code Playgroud)

-要么-

第二个建议的解决方法是通过分离实体,更新实例Address,然后重新附加它来完成的.在我的实现中,我对这个解决方法没有太多运气,但会将其发布给后代.也许你会比我更幸运.

context.Entry(employee.Address).State = EntityState.Detached;
employee.SetAddress(newAddress);
context.Entry(employee.Address).State = EntityState.Modified;
Run Code Online (Sandbox Code Playgroud)

UPDATE

我终于找到了EF Core团队的开放票,可以跟踪此问题.机票#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
}
Run Code Online (Sandbox Code Playgroud)

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

  • 受第二个选项的启发,这在更新实体时对我有用:`var existing = _db.Entities.Find(ent.ID);` 找到原始的 `_dbContext.Entry(existing).CurrentValues.SetValues(ent);`更新所有基本值 `existing.ChangeValueObject(ent.ValueObject);` 手动更新值对象 (2认同)