C# 实体框架不保存修改后的孩子
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/18054798/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Entity Framework not saving modified children
提问by Bob Tway
Frustrating, this. Here's a pair of related objects, as generated by database-first Entity Framework:
郁闷,这个。这是由数据库优先实体框架生成的一对相关对象:
public partial class DevelopmentType
{
public DevelopmentType()
{
this.DefaultCharges = new HashSet<DefaultCharge>();
}
public System.Guid RowId { get; set; }
public string Type { get; set; }
public virtual ICollection<DefaultCharge> DefaultCharges { get; set; }
}
public partial class DefaultCharge
{
public System.Guid RowId { get; set; }
public decimal ChargeableRate { get; set; }
public Nullable<System.Guid> DevelopmentType_RowId { get; set; }
public virtual DevelopmentType DevelopmentType { get; set; }
}
Here's the code that I'm calling to save a DevelopmentType - it involves automapper since we differentiate entity objects from DTOs:
这是我调用以保存 DevelopmentType 的代码 - 它涉及自动映射器,因为我们将实体对象与 DTO 区分开来:
public void SaveDevelopmentType(DevelopmentType_dto dt)
{
Entities.DevelopmentType mappedDevType = Mapper.Map<DevelopmentType_dto, Entities.DevelopmentType>(dt);
_Context.Entry(mappedDevType).State = System.Data.EntityState.Modified;
_Context.DevelopmentTypes.Attach(mappedDevType);
_Context.SaveChanges();
}
In my user interface, the most common operation will be for a user to look at a list of DevelopmentTypes and update their DefaultCharge. So when I test this using the above code, it runs without error, but nothing actually changes.
在我的用户界面中,最常见的操作是让用户查看 DevelopmentType 列表并更新他们的 DefaultCharge。所以当我使用上面的代码测试它时,它运行没有错误,但实际上没有任何变化。
If I pause in the debugger it's clear that the changed DefaultCharge is being passed into the function, and that it's attached to the DevelopmentType to be saved.
如果我在调试器中暂停,很明显更改的 DefaultCharge 正在传递到函数中,并且它附加到要保存的 DevelopmentType。
Stepping through it, if I change the value manually inside visual studio, it doessave the updated value. Which is just even more confusing.
逐步完成,如果我在 Visual Studio 中手动更改值,它会保存更新后的值。这更令人困惑。
Monitoring the database with SQL Server Profiler reveals that update commands are issued onlyfor the parent object and notfor any attached objects.
使用 SQL Server Profiler 监视数据库显示更新命令仅针对父对象而不是任何附加对象发出。
I have other similar code elsewhere that functions as expected. What am I doing wrong here?
我在其他地方还有其他类似的代码可以按预期运行。我在这里做错了什么?
EDIT:
编辑:
I have discovered that if you do this prior to the call to SaveDevelopmentType:
我发现如果您在调用 SaveDevelopmentType 之前执行此操作:
using (TransactionScope scope = new TransactionScope())
{
dt.Type = "Test1";
dt.DefaultCharges.First().ChargeableRate = 99;
_CILRepository.SaveDevelopmentType(dt);
scope.Complete();
}
The change to Type saves, but the change to ChargeableRate does not. I don't think it helps, massively, but thought I'd add it.
对 Type 的更改会保存,但对 ChargeableRate 的更改不会。我不认为它有很大帮助,但我想我会添加它。
采纳答案by boindiil
The problem is, that EF is not aware of the changed DefaultCharges.
问题是,EF 不知道更改的 DefaultCharges。
By setting the State of the DevelopmentType
to EntityState.Modified
, EF only knows that the object DevelopmentType
has been changed. However, this means that EF will only update DevelopmentType
but not it's navigation properties.
通过将 State 设置DevelopmentType
为EntityState.Modified
,EF 只知道该对象DevelopmentType
已更改。但是,这意味着 EF 只会更新DevelopmentType
而不是它的导航属性。
A workaround - which isn't best practice - would be to iterate over all DefaultCharge
of the current DevelopmentType
and set the entity state to EntityState.Modified
.
一种解决方法 - 这不是最佳实践 - 将迭代所有DefaultCharge
当前DevelopmentType
并将实体状态设置为EntityState.Modified
.
Additionally I would recommend to attach the entity to the context first, and change the state afterwards.
此外,我建议先将实体附加到上下文,然后再更改状态。
EDIT after comment
评论后编辑
As you are using DTOs I suppose you are transfering these objects either through different layers or different machines.
当您使用 DTO 时,我想您正在通过不同的层或不同的机器传输这些对象。
In this case I would recommend to use self tracking entities, because it is not possible to share one context. These entities additionally holds their current state (ie. new, updated, deleted etc). There are many tutorials on the net about self tracking entities.
在这种情况下,我建议使用自我跟踪实体,因为不可能共享一个上下文。这些实体还保持其当前状态(即新的、更新的、删除的等)。网上有很多关于自我跟踪实体的教程。
回答by haim770
Context.Entry()
already "Attaches" the Entity internally in order to have the context change its EntityState
.
Context.Entry()
已经在内部“附加”实体,以便让上下文更改其EntityState
.
By calling Attach()
you're changing the EntityState
back to Unchanged
. Try to comment out this line.
通过调用Attach()
您将EntityState
返回更改为Unchanged
. 尝试注释掉这一行。
回答by JTMon
As far as I know EF can save child entities only if the parent object was retrieved with the same Context that is trying to save it. That is attaching an object that was retrieved by one context to another context, will allow you to save changes to parent objects but not children. This was the result of a on old search based on which we switched to NHibernate. If memory serves correctly I was able to find a link where EF team member(s) confirmed this and that there WAS no plan to change this behavior. Unfortunately all links related to that search have been erased from my PC since.
据我所知,只有在使用试图保存它的相同上下文检索父对象时,EF 才能保存子实体。即将一个由一个上下文检索到的对象附加到另一个上下文,将允许您保存对父对象的更改,但不能保存对子对象的更改。这是基于我们切换到 NHibernate 的旧搜索的结果。如果没记错的话,我可以找到一个链接,EF 团队成员确认了这一点,并且没有计划改变这种行为。不幸的是,从那以后,与该搜索相关的所有链接都已从我的 PC 中删除。
As I am not aware of how you are retrieving the objects in your case, I am not sure this is relevant to your case, but put it out there just in case it helps.
由于我不知道您如何检索案例中的对象,我不确定这是否与您的案例相关,但将其放在那里以防万一。
Here is a link on attaching detached objects to a context.
这是将分离的对象附加到上下文的链接。
http://www.codeproject.com/Articles/576330/Attaching-detached-POCO-to-EF-DbContext-simple-and
http://www.codeproject.com/Articles/576330/Attaching-detached-POCO-to-EF-DbContext-simple-and
回答by jocull
This is not a workaround for every case, but I did discover that you can get around this by updating foreign keys on an object instead of updating navigation property objects.
这不是适用于所有情况的解决方法,但我确实发现您可以通过更新对象上的外键而不是更新导航属性对象来解决这个问题。
For example... instead of:
例如...而不是:
myObject.myProperty = anotherPropertyObject;
Try this:
尝试这个:
myObject.myPropertyID = anotherPropertyObject.ID;
Make sure the object is flagged as modified in EF's mind (as mentioned in other posts) and then call your save method.
确保该对象在 EF 的脑海中被标记为已修改(如其他帖子中所述),然后调用您的保存方法。
Worked for me at least! It'll be a no-go when working with nested properties, but perhaps you can break your contexts up into smaller chunks and work over objects in multiple parts to avoid context bloat.
至少对我有用!在使用嵌套属性时,这将是不可行的,但也许您可以将上下文分解成更小的块,并在多个部分处理对象以避免上下文膨胀。
Good luck! :)
祝你好运!:)
回答by Javier
The Graphdifflibrary was a great help for me to handle all of these complexities.
该Graphdiff库是一个很大的帮助,我来处理所有这些复杂的。
You only need to set up the navigation properties that you wish to insert/update/delete (using fluent syntax) and Graphdiff will take care of it
您只需要设置您希望插入/更新/删除的导航属性(使用流畅的语法),Graphdiff 会处理它
Note: It seems to be that the project is not updated anymore but i'm using it since more than a year and is quite stable
注意:这个项目好像没有更新了,不过我用了一年多了,还挺稳定的
回答by Javier
If I understand the question correctly, you have problem updating child fields. I had problems with child collection fields. I tried this and it worked for me. You should update all child collections after attaching the object to the database context change the modified state of the parent object and save changes to the context.
如果我正确理解了这个问题,则您在更新子字段时遇到了问题。我遇到了子集合字段的问题。我试过这个,它对我有用。在将对象附加到数据库上下文更改父对象的修改状态并将更改保存到上下文后,您应该更新所有子集合。
Database.Products.Attach(argProduct);
argProduct.Categories = Database.Categories.Where(x => ListCategories.Contains(x.CategoryId)).ToList();
Database.Entry(argProduct).State = EntityState.Modified;
Database.SaveChanges();
回答by Bryan
I created a helper method to solve this problem.
我创建了一个辅助方法来解决这个问题。
Consider this:
考虑一下:
public abstract class BaseEntity
{
/// <summary>
/// The unique identifier for this BaseEntity.
/// </summary>
[Key]
public Guid Id { get; set; }
}
public class BaseEntityComparer : IEqualityComparer<BaseEntity>
{
public bool Equals(BaseEntity left, BaseEntity right)
{
if (ReferenceEquals(null, right)) { return false; }
return ReferenceEquals(left, right) || left.Id.Equals(right.Id);
}
public int GetHashCode(BaseEntity obj)
{
return obj.Id.GetHashCode();
}
}
public class Event : BaseEntity
{
[Required(AllowEmptyStrings = false)]
[StringLength(256)]
public string Name { get; set; }
public HashSet<Manager> Managers { get; set; }
}
public class Manager : BaseEntity
{
[Required(AllowEmptyStrings = false)]
[StringLength(256)]
public string Name { get; set; }
public Event Event{ get; set; }
}
DbContext with the helper method:
带有辅助方法的 DbContext:
public class MyDataContext : DbContext
{
public MyDataContext() : base("ConnectionName") { }
//Tables
public DbSet<Event> Events { get; set; }
public DbSet<Manager> Managers { get; set; }
public async Task AddOrUpdate<T>(T entity, params string[] ignoreProperties) where T : BaseEntity
{
if (entity == null || Entry(entity).State == EntityState.Added || Entry(entity).State == EntityState.Modified) { return; }
var state = await Set<T>().AnyAsync(x => x.Id == entity.Id) ? EntityState.Modified : EntityState.Added;
Entry(entity).State = state;
var type = typeof(T);
RelationshipManager relationship;
var stateManager = ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager;
if (stateManager.TryGetRelationshipManager(entity, out relationship))
{
foreach (var end in relationship.GetAllRelatedEnds())
{
var isForeignKey = end.GetType().GetProperty("IsForeignKey", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(end) as bool?;
var navigationProperty = end.GetType().GetProperty("NavigationProperty", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(end);
var propertyName = navigationProperty?.GetType().GetProperty("Identity", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(navigationProperty) as string;
if (string.IsNullOrWhiteSpace(propertyName) || ignoreProperties.Contains(propertyName)) { continue; }
var property = type.GetProperty(propertyName);
if (property == null) { continue; }
if (end is IEnumerable) { await UpdateChildrenInternal(entity, property, isForeignKey == true); }
else { await AddOrUpdateInternal(entity, property, ignoreProperties); }
}
}
if (state == EntityState.Modified)
{
Entry(entity).OriginalValues.SetValues(await Entry(entity).GetDatabaseValuesAsync());
Entry(entity).State = GetChangedProperties(Entry(entity)).Any() ? state : EntityState.Unchanged;
}
}
private async Task AddOrUpdateInternal<T>(T entity, PropertyInfo property, params string[] ignoreProperties)
{
var method = typeof(EasementDataContext).GetMethod("AddOrUpdate");
var generic = method.MakeGenericMethod(property.PropertyType);
await (Task)generic.Invoke(this, new[] { property.GetValue(entity), ignoreProperties });
}
private async Task UpdateChildrenInternal<T>(T entity, PropertyInfo property, bool isForeignKey)
{
var type = typeof(T);
var method = isForeignKey ? typeof(EasementDataContext).GetMethod("UpdateForeignChildren") : typeof(EasementDataContext).GetMethod("UpdateChildren");
var objType = property.PropertyType.GetGenericArguments()[0];
var enumerable = typeof(IEnumerable<>).MakeGenericType(objType);
var param = Expression.Parameter(type, "x");
var body = Expression.Property(param, property);
var lambda = Expression.Lambda(Expression.Convert(body, enumerable), property.Name, new[] { param });
var generic = method.MakeGenericMethod(type, objType);
await (Task)generic.Invoke(this, new object[] { entity, lambda, null });
}
public async Task UpdateForeignChildren<T, TProperty>(T parent, Expression<Func<T, IEnumerable<TProperty>>> childSelector, IEqualityComparer<TProperty> comparer = null) where T : BaseEntity where TProperty : BaseEntity
{
var children = (childSelector.Invoke(parent) ?? Enumerable.Empty<TProperty>()).ToList();
foreach (var child in children) { await AddOrUpdate(child); }
var existingChildren = await Set<T>().Where(x => x.Id == parent.Id).SelectMany(childSelector).AsNoTracking().ToListAsync();
if (comparer == null) { comparer = new BaseEntityComparer(); }
foreach (var child in existingChildren.Except(children, comparer)) { Entry(child).State = EntityState.Deleted; }
}
public async Task UpdateChildren<T, TProperty>(T parent, Expression<Func<T, IEnumerable<TProperty>>> childSelector, IEqualityComparer<TProperty> comparer = null) where T : BaseEntity where TProperty : BaseEntity
{
var stateManager = ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager;
var currentChildren = childSelector.Invoke(parent) ?? Enumerable.Empty<TProperty>();
var existingChildren = await Set<T>().Where(x => x.Id == parent.Id).SelectMany(childSelector).AsNoTracking().ToListAsync();
if (comparer == null) { comparer = new BaseEntityComparer(); }
var addedChildren = currentChildren.Except(existingChildren, comparer).AsEnumerable();
var deletedChildren = existingChildren.Except(currentChildren, comparer).AsEnumerable();
foreach (var child in currentChildren) { await AddOrUpdate(child); }
foreach (var child in addedChildren) { stateManager.ChangeRelationshipState(parent, child, childSelector.Name, EntityState.Added); }
foreach (var child in deletedChildren)
{
Entry(child).State = EntityState.Unchanged;
stateManager.ChangeRelationshipState(parent, child, childSelector.Name, EntityState.Deleted);
}
}
public static IEnumerable<string> GetChangedProperties(DbEntityEntry dbEntry)
{
var propertyNames = dbEntry.State == EntityState.Added ? dbEntry.CurrentValues.PropertyNames : dbEntry.OriginalValues.PropertyNames;
foreach (var propertyName in propertyNames)
{
if (IsValueChanged(dbEntry, propertyName))
{
yield return propertyName;
}
}
}
private static bool IsValueChanged(DbEntityEntry dbEntry, string propertyName)
{
return !Equals(OriginalValue(dbEntry, propertyName), CurrentValue(dbEntry, propertyName));
}
private static string OriginalValue(DbEntityEntry dbEntry, string propertyName)
{
string originalValue = null;
if (dbEntry.State == EntityState.Modified)
{
originalValue = dbEntry.OriginalValues.GetValue<object>(propertyName) == null
? null
: dbEntry.OriginalValues.GetValue<object>(propertyName).ToString();
}
return originalValue;
}
private static string CurrentValue(DbEntityEntry dbEntry, string propertyName)
{
string newValue;
try
{
newValue = dbEntry.CurrentValues.GetValue<object>(propertyName) == null
? null
: dbEntry.CurrentValues.GetValue<object>(propertyName).ToString();
}
catch (InvalidOperationException) // It will be invalid operation when its in deleted state. in that case, new value should be null
{
newValue = null;
}
return newValue;
}
}
Then I call it like this
然后我这样称呼它
// POST: Admin/Events/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit(Event @event)
{
if (!ModelState.IsValid) { return View(@event); }
await _db.AddOrUpdate(@event);
await _db.SaveChangesAsync();
return RedirectToAction("Index");
}