我有一个实体,它有一个值对象,这个值对象有另一个值对象。我的问题是,当将实体与值对象一起更新时,具有父值对象的实体会更新,但子值对象不会更新。 请注意,我使用了最新版本的实体框架核心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; }
}
更新员工时,我首先从数据库中获取它,然后使用从用户界面获取的新值更改Address
属性。
var employee = _repository.GetEmployee(empId);
employee.SetAddress(newAddress);
和SetAddress
方法:
public void SetAddress(Address address)
{
Guard.AssertArgumentNotNull(address, nameof(address));
Address = address;
}
根据此 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;
// ...
}
-或-
第二个建议的解决方法是通过分离实体,更新Address
的实例,然后重新附加它来完成。在我的实现中,我对这种解决方法没有太多运气,但会将其发布给后代。也许你会比我运气更好。
context.Entry(employee.Address).State = EntityState.Detached;
employee.SetAddress(newAddress);
context.Entry(employee.Address).State = EntityState.Modified;
更新
我终于找到了可以跟踪此问题的 EF Core 团队的开放票证。工单 #10551 特别说明了手头的问题,并且仍然未决。它绝对没有进入EF Core 2.1,并且似乎已被放置在积压工作里程碑3.0中。请注意,你可以对此问题投赞成票,作为让 EF Core 团队对此给予更多关注的一种方式。
UPDATE2 EF Core 2.2 引入了跟踪图组件,使此操作更加流畅。 但是,这确实要求所有 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 Core 中为修改的实体保留的拥有类型属性中找到更简单的替代方法
简而言之,而不是
_context.Entry(contactModelFromRequestBody).State = EntityState.Modified;
你应该使用
_context.Update(contactModelFromRequestBody);