这两个实体是一对多关系(由代码第一个fluent api构建)。
public class Parent
{
public Parent()
{
this.Children = new List<Child>();
}
public int Id { get; set; }
public virtual ICollection<Child> Children { get; set; }
}
public class Child
{
public int Id { get; set; }
public int ParentId { get; set; }
public string Data { get; set; }
}
在我的WebApi控制器中,我有创建父实体(工作正常)和更新父实体(有一些问题)的操作。更新操作如下所示:
public void Update(UpdateParentModel model)
{
//what should be done here?
}
目前我有两个想法:
获取一个被跟踪的父实体,命名为按模型存在的。Id,并将模型中的值逐个分配给实体。这听起来很愚蠢。在模型中。我不知道哪个子是新的,哪个子是修改的(甚至是删除的)。
通过模型创建一个新的父实体,并将其附加到DbContext并保存。但是DbContext如何知道子节点的状态(新增/删除/修改)呢?
实现这个功能的正确方法是什么?
我一直在摆弄这样的东西……
protected void UpdateChildCollection<Tparent, Tid , Tchild>(Tparent dbItem, Tparent newItem, Func<Tparent, IEnumerable<Tchild>> selector, Func<Tchild, Tid> idSelector) where Tchild : class
{
var dbItems = selector(dbItem).ToList();
var newItems = selector(newItem).ToList();
if (dbItems == null && newItems == null)
return;
var original = dbItems?.ToDictionary(idSelector) ?? new Dictionary<Tid, Tchild>();
var updated = newItems?.ToDictionary(idSelector) ?? new Dictionary<Tid, Tchild>();
var toRemove = original.Where(i => !updated.ContainsKey(i.Key)).ToArray();
var removed = toRemove.Select(i => DbContext.Entry(i.Value).State = EntityState.Deleted).ToArray();
var toUpdate = original.Where(i => updated.ContainsKey(i.Key)).ToList();
toUpdate.ForEach(i => DbContext.Entry(i.Value).CurrentValues.SetValues(updated[i.Key]));
var toAdd = updated.Where(i => !original.ContainsKey(i.Key)).ToList();
toAdd.ForEach(i => DbContext.Set<Tchild>().Add(i.Value));
}
你可以这样调用:
UpdateChildCollection(dbCopy, detached, p => p.MyCollectionProp, collectionItem => collectionItem.Id)
不幸的是,如果子类型上有集合属性也需要更新,这种方法就会失败。考虑通过传递一个IRepository(带有基本的CRUD方法)来解决这个问题,这个IRepository将负责自己调用UpdateChildCollection。调用repo,而不是直接调用DbContext.Entry。
不知道这将如何大规模地执行,但不确定还能做什么来解决这个问题。
下面的代码片段来自我的一个项目,我在其中实现了同样的事情。它将保存数据,如果有新的条目,更新,如果记录是不可用的张贴json。
Json数据来帮助你理解模式:
{
"groupId": 1,
"groupName": "Group 1",
"sortOrder": 1,
"filterNames": [
{
"filterId": 1,
"filterName1": "Name11111",
"sortOrder": 10,
"groupId": 1
} ,
{
"filterId": 1006,
"filterName1": "Name Changed 1",
"sortOrder": 10,
"groupId": 1
} ,
{
"filterId": 1007,
"filterName1": "New Filter 1",
"sortOrder": 10,
"groupId": 1
} ,
{
"filterId": 2,
"filterName1": "Name 2 Changed",
"sortOrder": 10,
"groupId": 1
}
]
}
public async Task<int> UpdateFilter(FilterGroup filterGroup)
{
var Ids = from f in filterGroup.FilterNames select f.FilterId;
var toBeDeleted = dbContext.FilterNames.Where(x => x.GroupId == filterGroup.GroupId
&& !Ids.Contains(x.FilterId)).ToList();
foreach(var item in toBeDeleted)
{
dbContext.FilterNames.Remove(item);
}
await dbContext.SaveChangesAsync();
dbContext.FilterGroups.Attach(filterGroup);
dbContext.Entry(filterGroup).State = EntityState.Modified;
for(int i=0;i<filterGroup.FilterNames.Count();i++)
{
if (filterGroup.FilterNames.ElementAt(i).FilterId != 0)
{
dbContext.Entry(filterGroup.FilterNames.ElementAt(i)).State = EntityState.Modified;
}
}
return await dbContext.SaveChangesAsync();
}
如果你正在使用EntityFrameworkCore,你可以在你的控制器post动作中做以下事情(Attach方法递归地附加导航属性,包括集合):
_context.Attach(modelPostedToController);
IEnumerable<EntityEntry> unchangedEntities = _context.ChangeTracker.Entries().Where(x => x.State == EntityState.Unchanged);
foreach(EntityEntry ee in unchangedEntities){
ee.State = EntityState.Modified;
}
await _context.SaveChangesAsync();
假设每个被更新的实体都设置了所有属性,并在来自客户端的post数据中提供。不会为实体的部分更新工作)。
您还需要确保为该操作使用了一个新的/专用的实体框架数据库上下文。
So, I finally managed to get it working, although not fully automatically.
Notice the AutoMapper <3. It handles all the mapping of properties so you don't have to do it manually. Also, if used in a way where it maps from one object to another, then it only updates the properties and that marks changed properties as Modified to EF, which is what we want.
If you would use explicit context.Update(entity), the difference would be that entire object would be marked as Modified and EVERY prop would be updated.
In that case you don't need tracking but the drawbacks are as mentioned.
Maybe that's not a problem for you but it's more expensive and I want to log exact changes inside Save so I need correct info.
// We always want tracking for auto-updates
var entityToUpdate = unitOfWork.GetGenericRepository<Article, int>()
.GetAllActive() // Uses EF tracking
.Include(e => e.Barcodes.Where(e => e.Status == DatabaseEntityStatus.Active))
.First(e => e.Id == request.Id);
mapper.Map(request, entityToUpdate); // Maps it to entity with AutoMapper <3
ModifyBarcodes(entityToUpdate, request);
// Removed part of the code for space
unitOfWork.Save();
ModifyBarcodes部分在这里。
我们希望以一种EF跟踪不会被打乱的方式修改集合。
不幸的是,AutoMapper映射会创建一个全新的集合实例,因此会搞砸跟踪,尽管,我很确定它应该工作。
无论如何,因为我从FE发送完整的列表,在这里我们实际上决定了应该添加/更新/删除什么,只是处理列表本身。
由于EF跟踪是打开的,EF处理它就像一个魅力。
var toUpdate = article.Barcodes
.Where(e => articleDto.Barcodes.Select(b => b.Id).Contains(e.Id))
.ToList();
toUpdate.ForEach(e =>
{
var newValue = articleDto.Barcodes.FirstOrDefault(f => f.Id == e.Id);
mapper.Map(newValue, e);
});
var toAdd = articleDto.Barcodes
.Where(e => !article.Barcodes.Select(b => b.Id).Contains(e.Id))
.Select(e => mapper.Map<Barcode>(e))
.ToList();
article.Barcodes.AddRange(toAdd);
article.Barcodes
.Where(e => !articleDto.Barcodes.Select(b => b.Id).Contains(e.Id))
.ToList()
.ForEach(e => article.Barcodes.Remove(e));
CreateMap<ArticleDto, Article>()
.ForMember(e => e.DateCreated, opt => opt.Ignore())
.ForMember(e => e.DateModified, opt => opt.Ignore())
.ForMember(e => e.CreatedById, opt => opt.Ignore())
.ForMember(e => e.LastModifiedById, opt => opt.Ignore())
.ForMember(e => e.Status, opt => opt.Ignore())
// When mapping collections, the reference itself is destroyed
// hence f* up EF tracking and makes it think all previous is deleted
// Better to leave it on manual and handle collecion manually
.ForMember(e => e.Barcodes, opt => opt.Ignore())
.ReverseMap()
.ForMember(e => e.Barcodes, opt => opt.MapFrom(src => src.Barcodes.Where(e => e.Status == DatabaseEntityStatus.Active)));
这不是最优雅的方法,但很有效。干杯!
var entity = await context.Entities.FindAsync(id);
var newEntity = new AmazingEntity() {
p1 = child1
p2 = child2
p3 = child3.child4 //... nested collections
};
if (entity != null)
{
db.Entities.Remove(entity);
}
db.Entities.Add(newEntity);
await db.SaveChangesAsync();
记住去掉PK。
var child4 = Tools.CloneJson(deepNestedElement);
child4.id = 0;
child3.Add(child4);
public static class Tools
{
public static JsonSerializerSettings jsonSettings = new JsonSerializerSettings {
ObjectCreationHandling = ObjectCreationHandling.Replace,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
};
public static string JSerialize<T>(T source) {
return JsonConvert.SerializeObject(source, Formatting.Indented, jsonSettings);
}
public static T JDeserialize<T>(string source) {
return JsonConvert.DeserializeObject<T>(source, jsonSettings);
}
public static T CloneJson<T>(this T source)
{
return CloneJson<T, T>(source);
}
public static TOut CloneJson<TIn, TOut>(TIn source)
{
if (Object.ReferenceEquals(source, null))
return default(TOut);
return JDeserialize<TOut>(JSerialize(source));
}
}