C# EF Core-在一个请求中添加/更新实体和添加/更新/删除子实体
我正在努力完成一些基本的操作 假设我有一个名为Master的类:C# EF Core-在一个请求中添加/更新实体和添加/更新/删除子实体,c#,entity-framework,asp.net-core,entity-framework-core,aspnetboilerplate,C#,Entity Framework,Asp.net Core,Entity Framework Core,Aspnetboilerplate,我正在努力完成一些基本的操作 假设我有一个名为Master的类: public class Master { public Master() { Children = new List<Child>(); } public int Id { get; set; } public string SomeProperty { get; set; } [ForeignKey("SuperMasterId")] pu
public class Master
{
public Master()
{
Children = new List<Child>();
}
public int Id { get; set; }
public string SomeProperty { get; set; }
[ForeignKey("SuperMasterId")]
public SuperMaster SuperMaster { get; set; }
public int SuperMasterId { get; set; }
public ICollection<Child> Children { get; set; }
}
public class Child
{
public int Id { get; set; }
public string SomeDescription { get; set; }
public decimal Count{ get; set; }
[ForeignKey("RelatedEntityId")]
public RelatedEntity RelatedEntity { get; set; }
public int RelatedEntityId { get; set; }
[ForeignKey("MasterId")]
public Master Master { get; set; }
public int MasterId { get; set; }
}
Masters
表当前有一条Id为10的记录。它没有孩子
引发的异常是:
Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException:数据库操作预期影响1行,但实际影响0行。自加载实体后,数据可能已被修改或删除。
这是怎么回事?我认为EF应该跟踪更改,这包括知道我们在该内部方法中调用了SaveChanges
编辑删除保存调用不会改变任何内容。另外,在观察SQLServer事件探查器中发生的情况时,我找不到任何由EF生成的INSERT或UPDATE SQL语句
EDIT2调用SaveChanges时,INSERT语句在那里,但主实体仍然没有UPDATE语句。像往常一样,将此问题发布到StackOverflow帮助我解决了问题。代码最初看起来与上面的问题不一样,但我在编写问题时正在修改代码 在写问题之前,我花了将近一天的时间试图找出问题所在,因此我尝试了不同的方法,例如重新创建实体实例并手动附加它们,将某些实体标记为未更改/修改,使用AsNoTracking甚至完全禁用所有实体的自动更改跟踪,并手动标记所有添加或修改的实体 结果表明,导致此问题的代码位于该子存储库的一个私有方法中,我忽略了该方法,因为我认为它与此无关。不过,如果我没有忘记从中删除一些手动更改跟踪代码的话,它就真的不相关了。这些代码基本上是在摆弄EF的自动更改跟踪程序,导致其行为不端 但是,多亏了StackOverflow,问题才得以解决。当你和某人谈论这个问题时,你需要自己重新分析它,能够解释它的所有细节,以便与你交谈的人(在本例中是SO社区)理解它。当您重新分析它时,您会注意到所有导致bits的小问题,这样就更容易诊断问题 无论如何,如果有人因为标题而被这个问题吸引,通过谷歌搜索或w/e,这里有一些要点:
- 如果要在多个级别上更新实体,请在获取现有实体时始终调用
,以包括所有相关的导航属性。这将使它们全部加载到更改跟踪器中,您无需手动附加/标记。更新完成后,调用SaveChanges将正确保存所有更改.Include
- 当您需要更新子实体时,不要对顶级实体使用AutoMapper,尤其是当您在更新子实体时必须实现一些附加逻辑时
- 永远不要像我在将Id设置为-1时尝试的那样更新主键,也不要像我在控制器更新方法中的这一行尝试的那样更新主键:
//映射程序还将更新child.RelatedEntity.Id Mapper.Map(child,oldChild)代码>
- 如果需要处理已删除的项目,最好检测它们并存储在单独的列表中,然后手动为每个项目调用repository delete方法,其中repository delete方法将包含有关相关实体的一些最终附加逻辑
- 如果需要更改相关实体的主键,则需要首先从关系中删除该相关实体,然后添加一个具有更新键的新实体
public async Task<OutputDto> Update(InputDto input)
{
// First get a real entity by Id from the repository
// This repository method returns:
// Context.Masters
// .Include(x => x.SuperMaster)
// .Include(x => x.Children)
// .ThenInclude(x => x.RelatedEntity)
// .FirstOrDefault(x => x.Id == id)
Master entity = await _masterRepository.Get(input.Id);
// Update the master entity properties manually
entity.SomeProperty = "Updated value";
// Prepare a list for any children with modified RelatedEntity
var changedChildren = new List<Child>();
foreach (var child in input.Children)
{
// Check to see if this is a new child item
if (entity.Children.All(x => x.Id != child.Id))
{
// Map the DTO to child entity and add it to the collection
entity.Children.Add(Mapper.Map<Child>(child));
continue;
}
// Check to see if this is an existing child item
var existingChild = entity.Children.FirstOrDefault(x => x.Id == child.Id);
if (existingChild == null)
{
continue;
}
// Check to see if the related entity was changed
if (existingChild.RelatedEntity.Id != child.RelatedEntity.Id)
{
// It was changed, add it to changedChildren list
changedChildren.Add(existingChild);
continue;
}
// It's safe to use AutoMapper to map the child entity and avoid updating properties manually,
// provided that it doesn't have child-items of their own
Mapper.Map(child, existingChild);
}
// Find which of the child entities should be deleted
// entity.IsTransient() is an extension method which returns true if the entity has just been added
foreach (var child in entity.Children.Where(x => !x.IsTransient()).ToList())
{
if (input.Children.Any(x => x.Id == child.Id))
{
continue;
}
// We don't have this entity in the list sent by the client.
// That means we should delete it
await _childRepository.DeleteAsync(child);
entity.Children.Remove(child);
}
// Parse children entities with modified related entities
foreach (var child in changedChildren)
{
var newChild = input.Children.FirstOrDefault(x => x.Id == child.Id);
// Delete the existing one
await _childRepository.DeleteAsync(child);
entity.Children.Remove(child);
// Add the new one
// It's OK to change the primary key here, as this one is a DTO, not a tracked entity,
// and besides, if the keys are autogenerated by the database, we can't have anything but 0 for a new entity
newChild.Id = 0;
entity.Djelovi.Add(Mapper.Map<Child>(newChild));
}
// And finally, call the repository update and return the result mapped to DTO
entity = await _repository.UpdateAsync(entity);
return MapToEntityDto(entity);
}
公共异步任务更新(输入到输入)
{
//首先从存储库中按Id获取真实实体
//此存储库方法返回:
//背景。大师
//.包括(x=>x.SuperMaster)
//.包括(x=>x.儿童)
//.ThenInclude(x=>x.RelatedEntity)
//.FirstOrDefault(x=>x.Id==Id)
主实体=await\u masterRepository.Get(input.Id);
//手动更新主实体属性
entity.SomeProperty=“更新值”;
//为具有修改的RelatedEntity的所有子级准备一个列表
var changedChildren=新列表();
foreach(input.Children中的变量child)
{
//检查这是否是新的子项
if(entity.Children.All(x=>x.Id!=child.Id))
{
//将DTO映射到子实体并将其添加到集合中
entity.Children.Add(Mapper.Map(child));
继续;
}
//检查此项是否为现有子项
var existingChild=entity.Children.FirstOrDefault(x=>x.Id==child.Id);
if(existingChild==null)
{
继续;
}
//检查相关实体是否已更改
if(existingChild.RelatedEntity.Id!=child.RelatedEntity.Id)
{
//已更改,请将其添加到changedChildren列表中
changedChildren.Add(现有子级);
继续;
}
//使用AutoMapper映射子实体是安全的,避免手动更新属性,
//前提是它没有自己的子项
Mapper.Map(子级,现有子级);
}
//查找应删除的子实体
//IsTransient()是一个扩展方法,如果刚刚添加实体,则返回true
foreach(entity.Children.Where(x=>!x.IsTransient()).ToList()中的变量child)
{
if(input.Children.Any(x=>x.Id==child.Id))
{
继续;
}
//客户端发送的列表中没有此实体。
//这意味着我们
public async Task<Master> UpdateAsync(Master entity)
{
var superMasterId = entity.SuperMaster.Id;
// Make sure SuperMaster properties are updated in case the superMasterId is changed
entity.SuperMaster = await Context.SuperMasters
.FirstOrDefaultAsync(x => x.Id == superMasterId);
// New and updated children, skip deleted
foreach (var child in entity.Children.Where(x => x.Id != -1))
{
await _childRepo.InsertOrUpdateAsync(child);
}
// Handle deleted children
foreach (var child in entity.Children.Where(x => x.Id == -1))
{
await _childRepo.DeleteAsync(child);
entity.Children.Remove(child);
}
return entity;
}
public async Task<Child> InsertOrUpdateAsync(Child entity)
{
if (entity.Id == 0)
{
return await InsertAsync(entity, parent);
}
var relatedId = entity.RelatedEntity.Id;
entity.RelatedEntity = await Context.RelatedEntities
.FirstOrDefaultAsync(x => x.Id == relatedId);
// We have already updated child properties in the controller method
// and it's expected that changed entities are marked as changed in EF change tracker
return entity;
}
public async Task<Child> InsertAsync(Child entity)
{
var relatedId = entity.RelatedEntity.Id;
entity.RelatedEntity = await Context.RelatedEntities
.FirstOrDefaultAsync(x => x.Id == relatedId);
entity = Context.Set<Child>().Add(entity).Entity;
// We need the entity Id, hence the call to SaveChanges
await Context.SaveChangesAsync();
return entity;
}
{
"someProperty": "Some property",
"superMaster": {
"name": "SuperMaster name",
"id": 1
},
"children": [
{
"relatedEntity": {
"name": "RelatedEntity name",
"someOtherProp": 20,
"id": 1
},
"count": 20,
"someDescription": "Something"
}],
"id": 10
}
public async Task<OutputDto> Update(InputDto input)
{
// First get a real entity by Id from the repository
// This repository method returns:
// Context.Masters
// .Include(x => x.SuperMaster)
// .Include(x => x.Children)
// .ThenInclude(x => x.RelatedEntity)
// .FirstOrDefault(x => x.Id == id)
Master entity = await _masterRepository.Get(input.Id);
// Update the master entity properties manually
entity.SomeProperty = "Updated value";
// Prepare a list for any children with modified RelatedEntity
var changedChildren = new List<Child>();
foreach (var child in input.Children)
{
// Check to see if this is a new child item
if (entity.Children.All(x => x.Id != child.Id))
{
// Map the DTO to child entity and add it to the collection
entity.Children.Add(Mapper.Map<Child>(child));
continue;
}
// Check to see if this is an existing child item
var existingChild = entity.Children.FirstOrDefault(x => x.Id == child.Id);
if (existingChild == null)
{
continue;
}
// Check to see if the related entity was changed
if (existingChild.RelatedEntity.Id != child.RelatedEntity.Id)
{
// It was changed, add it to changedChildren list
changedChildren.Add(existingChild);
continue;
}
// It's safe to use AutoMapper to map the child entity and avoid updating properties manually,
// provided that it doesn't have child-items of their own
Mapper.Map(child, existingChild);
}
// Find which of the child entities should be deleted
// entity.IsTransient() is an extension method which returns true if the entity has just been added
foreach (var child in entity.Children.Where(x => !x.IsTransient()).ToList())
{
if (input.Children.Any(x => x.Id == child.Id))
{
continue;
}
// We don't have this entity in the list sent by the client.
// That means we should delete it
await _childRepository.DeleteAsync(child);
entity.Children.Remove(child);
}
// Parse children entities with modified related entities
foreach (var child in changedChildren)
{
var newChild = input.Children.FirstOrDefault(x => x.Id == child.Id);
// Delete the existing one
await _childRepository.DeleteAsync(child);
entity.Children.Remove(child);
// Add the new one
// It's OK to change the primary key here, as this one is a DTO, not a tracked entity,
// and besides, if the keys are autogenerated by the database, we can't have anything but 0 for a new entity
newChild.Id = 0;
entity.Djelovi.Add(Mapper.Map<Child>(newChild));
}
// And finally, call the repository update and return the result mapped to DTO
entity = await _repository.UpdateAsync(entity);
return MapToEntityDto(entity);
}
updatechild(objCas.ECC_Decision, PromatCon.ECC_Decision.Where(c => c.rid == objCas.rid & !objCas.ECC_Decision.Select(x => x.dcid).Contains(c.dcid)).toList())
public void updatechild<Ety>(ICollection<Ety> amList, ICollection<Ety> rList)
{
foreach (var obj in amList)
{
var x = PromatCon.Entry(obj).GetDatabaseValues();
if (x == null)
PromatCon.Entry(obj).State = EntityState.Added;
else
PromatCon.Entry(obj).State = EntityState.Modified;
}
foreach (var obj in rList.ToList())
PromatCon.Entry(obj).State = EntityState.Deleted;
}
PromatCon.SaveChanges()