这两个实体是一对多关系(由代码第一个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如何知道子节点的状态(新增/删除/修改)呢?
实现这个功能的正确方法是什么?
考虑使用https://github.com/WahidBitar/EF-Core-Simple-Graph-Update。
这对我来说很有效。
这个库很简单,实际上只有一个扩展方法
T InsertUpdateOrDeleteGraph<T>(this DbContext context,
T newEntity, T existingEntity)
https://github.com/WahidBitar/EF-Core-Simple-Graph-Update/blob/master/src/Diwink.Extensions.EntityFrameworkCore/DbContextExtensions.cs#L34
与这个问题的大多数答案相比,它是通用的(不使用硬编码的表名,可以用于不同的模型),
并包括针对不同模型更改的单元测试。
作者及时回应报告的问题。
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)));
我一直在摆弄这样的东西……
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。
不知道这将如何大规模地执行,但不确定还能做什么来解决这个问题。
VB。NET开发人员使用这个通用子标记子状态,易于使用
注:
PromatCon:实体对象
amList:要添加或修改的子列表
rList:要删除的子列表
updatechild(objCas.ECC_Decision, PromatCon.ECC_Decision.Where(Function(c) c.rid = objCas.rid And Not objCas.ECC_Decision.Select(Function(x) x.dcid).Contains(c.dcid)).toList)
Sub updatechild(Of Ety)(amList As ICollection(Of Ety), rList As ICollection(Of Ety))
If amList IsNot Nothing Then
For Each obj In amList
Dim x = PromatCon.Entry(obj).GetDatabaseValues()
If x Is Nothing Then
PromatCon.Entry(obj).State = EntityState.Added
Else
PromatCon.Entry(obj).State = EntityState.Modified
End If
Next
End If
If rList IsNot Nothing Then
For Each obj In rList.ToList
PromatCon.Entry(obj).State = EntityState.Deleted
Next
End If
End Sub
PromatCon.SaveChanges()
因为我讨厌重复复杂的逻辑,这里有一个Slauma解决方案的通用版本。
这是我的更新方法。请注意,在分离场景中,有时您的代码将读取数据,然后更新它,因此它并不总是分离的。
public async Task UpdateAsync(TempOrder order)
{
order.CheckNotNull(nameof(order));
order.OrderId.CheckNotNull(nameof(order.OrderId));
order.DateModified = _dateService.UtcNow;
if (_context.Entry(order).State == EntityState.Modified)
{
await _context.SaveChangesAsync().ConfigureAwait(false);
}
else // Detached.
{
var existing = await SelectAsync(order.OrderId!.Value).ConfigureAwait(false);
if (existing != null)
{
order.DateModified = _dateService.UtcNow;
_context.TrackChildChanges(order.Products, existing.Products, (a, b) => a.OrderProductId == b.OrderProductId);
await _context.SaveChangesAsync(order, existing).ConfigureAwait(false);
}
}
}
这里定义了CheckNotNull。
创建这些扩展方法。
/// <summary>
/// Tracks changes on childs models by comparing with latest database state.
/// </summary>
/// <typeparam name="T">The type of model to track.</typeparam>
/// <param name="context">The database context tracking changes.</param>
/// <param name="childs">The childs to update, detached from the context.</param>
/// <param name="existingChilds">The latest existing data, attached to the context.</param>
/// <param name="match">A function to match models by their primary key(s).</param>
public static void TrackChildChanges<T>(this DbContext context, IList<T> childs, IList<T> existingChilds, Func<T, T, bool> match)
where T : class
{
context.CheckNotNull(nameof(context));
childs.CheckNotNull(nameof(childs));
existingChilds.CheckNotNull(nameof(existingChilds));
// Delete childs.
foreach (var existing in existingChilds.ToList())
{
if (!childs.Any(c => match(c, existing)))
{
existingChilds.Remove(existing);
}
}
// Update and Insert childs.
var existingChildsCopy = existingChilds.ToList();
foreach (var item in childs.ToList())
{
var existing = existingChildsCopy
.Where(c => match(c, item))
.SingleOrDefault();
if (existing != null)
{
// Update child.
context.Entry(existing).CurrentValues.SetValues(item);
}
else
{
// Insert child.
existingChilds.Add(item);
// context.Entry(item).State = EntityState.Added;
}
}
}
/// <summary>
/// Saves changes to a detached model by comparing it with the latest data.
/// </summary>
/// <typeparam name="T">The type of model to save.</typeparam>
/// <param name="context">The database context tracking changes.</param>
/// <param name="model">The model object to save.</param>
/// <param name="existing">The latest model data.</param>
public static void SaveChanges<T>(this DbContext context, T model, T existing)
where T : class
{
context.CheckNotNull(nameof(context));
model.CheckNotNull(nameof(context));
context.Entry(existing).CurrentValues.SetValues(model);
context.SaveChanges();
}
/// <summary>
/// Saves changes to a detached model by comparing it with the latest data.
/// </summary>
/// <typeparam name="T">The type of model to save.</typeparam>
/// <param name="context">The database context tracking changes.</param>
/// <param name="model">The model object to save.</param>
/// <param name="existing">The latest model data.</param>
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
/// <returns></returns>
public static async Task SaveChangesAsync<T>(this DbContext context, T model, T existing, CancellationToken cancellationToken = default)
where T : class
{
context.CheckNotNull(nameof(context));
model.CheckNotNull(nameof(context));
context.Entry(existing).CurrentValues.SetValues(model);
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
下面的代码片段来自我的一个项目,我在其中实现了同样的事情。它将保存数据,如果有新的条目,更新,如果记录是不可用的张贴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();
}