说我有一个Sale
课:
public class Sale : BaseEntity //BaseEntity only has an Id
{
public ICollection<Item> Items { get; set; }
}
还有一个Item
类:
public class Item : BaseEntity //BaseEntity only has an Id
{
public int SaleId { get; set; }
public Sale Sale { get; set; }
}
和通用存储库(更新方法):
public async Task<int> UpdateAsync<T>(T entity, params Expression<Func<T, object>>[] navigations) where T : BaseEntity
{
var dbEntity = _dbContext.Set<T>().Find(entity.Id);
var dbEntry = _dbContext.Entry(dbEntity);
dbEntry.CurrentValues.SetValues(entity);
foreach (var property in navigations)
{
var propertyName = property.GetPropertyAccess().Name;
await dbEntry.Collection(propertyName).LoadAsync();
List<BaseEntity> dbChilds = dbEntry.Collection(propertyName).CurrentValue.Cast<BaseEntity>().ToList();
foreach (BaseEntity child in dbChilds)
{
if (child.Id == 0)
{
_dbContext.Entry(child).State = EntityState.Added;
}
else
{
_dbContext.Entry(child).State = EntityState.Modified;
}
}
}
return await _dbContext.SaveChangesAsync();
}
我在更新Item
类上的Sale
集合时遇到困难。使用此代码,我成功地add
或modify
了Item
。但是,当我delete
层上的某个项目UI
时,什么也不会被删除。
在使用通用存储库模式时,EF Core
是否可以解决这种情况?
更新
似乎Items
跟踪丢失了。这是我的包含的通用检索方法。
public async Task<T> GetByIdAsync<T>(int id, params Expression<Func<T, object>>[] includes) where T : BaseEntity
{
var query = _dbContext.Set<T>().AsQueryable();
if (includes != null)
{
query = includes.Aggregate(query,
(current, include) => current.Include(include));
}
return await query.SingleOrDefaultAsync(e => e.Id == id);
}
答案 0 :(得分:4)
显然,问题是要对包含集合导航属性的disconnected entity进行修改(否则,除了调用SaveChanges
之外,您不需要做任何其他事情),该集合导航属性需要反映添加/删除/更新的项目从传递的对象中获取。
EF Core没有提供这种开箱即用的功能。对于具有自动生成键的实体,它通过Update
方法支持简单的upsert(插入或更新),但是不会检测和删除已删除的项目。
因此,您需要自己进行检测。加载现有项目是朝正确方向迈出的一步。您的代码的问题在于它没有考虑新项目,而是对从数据库中检索到的现有项目进行了无用的状态操作。
以下是相同想法的正确实现。它使用一些EF Core内部构件(由IClrCollectionAccessor
方法返回的GetCollectionAccessor()
-都需要using Microsoft.EntityFrameworkCore.Metadata.Internal;
)来操纵集合,但是您的代码已经在使用内部GetPropertyAccess()
方法,因此,我想这应该不成问题-万一在将来的EF Core版本中进行了更改,则应该相应地更新代码。之所以需要集合访问器是因为IEnumerable<BaseEntity>
由于协方差可以用于一般性地访问集合,而ICollection<BaseEntity>
的不变性就不能说相同了,我们需要一种访问{{1}的方法} / Add
方法。内部访问器提供了该功能以及从传递的实体中一般检索属性值的方法。
代码如下:
Remove
该算法非常标准。从数据库加载集合后,我们创建一个字典,其中包含由ID键入的现有项(用于快速查找)。然后,我们对新项目进行一次传递。我们使用字典查找对应的现有项目。如果未找到匹配项,则该项目被视为新项目,并且仅添加到目标(跟踪)集合中。否则,找到的项目将从源中更新,并从字典中删除。这样,在完成循环之后,词典中包含需要删除的项目,因此我们所需要做的就是将它们从目标(跟踪)集合中删除。
仅此而已。剩下的工作将由EF Core变更跟踪器完成-添加到目标集合的项目将标记为public async Task<int> UpdateAsync<T>(T entity, params Expression<Func<T, object>>[] navigations) where T : BaseEntity
{
var dbEntity = await _dbContext.FindAsync<T>(entity.Id);
var dbEntry = _dbContext.Entry(dbEntity);
dbEntry.CurrentValues.SetValues(entity);
foreach (var property in navigations)
{
var propertyName = property.GetPropertyAccess().Name;
var dbItemsEntry = dbEntry.Collection(propertyName);
var accessor = dbItemsEntry.Metadata.GetCollectionAccessor();
await dbItemsEntry.LoadAsync();
var dbItemsMap = ((IEnumerable<BaseEntity>)dbItemsEntry.CurrentValue)
.ToDictionary(e => e.Id);
var items = (IEnumerable<BaseEntity>)accessor.GetOrCreate(entity);
foreach (var item in items)
{
if (!dbItemsMap.TryGetValue(item.Id, out var oldItem))
accessor.Add(dbEntity, item);
else
{
_dbContext.Entry(oldItem).CurrentValues.SetValues(item);
dbItemsMap.Remove(item.Id);
}
}
foreach (var oldItem in dbItemsMap.Values)
accessor.Remove(dbEntity, oldItem);
}
return await _dbContext.SaveChangesAsync();
}
,更新的项目将标记为Added
或Unchanged
,并且删除的项目,取决于删除级联行为,将被标记为删除或更新(与父级取消关联)。如果要强制删除,只需替换
Modified
使用
accessor.Remove(dbEntity, oldItem);
答案 1 :(得分:2)
@craigmoliver这是我的解决方案。我知道这不是最好的-如果您找到更优雅的方式,请分享。
存储库:
public async Task<TEntity> UpdateAsync<TEntity, TId>(TEntity entity, bool save = true, params Expression<Func<TEntity, object>>[] navigations)
where TEntity : class, IIdEntity<TId>
{
TEntity dbEntity = await _context.FindAsync<TEntity>(entity.Id);
EntityEntry<TEntity> dbEntry = _context.Entry(dbEntity);
dbEntry.CurrentValues.SetValues(entity);
foreach (Expression<Func<TEntity, object>> property in navigations)
{
var propertyName = property.GetPropertyAccess().Name;
CollectionEntry dbItemsEntry = dbEntry.Collection(propertyName);
IClrCollectionAccessor accessor = dbItemsEntry.Metadata.GetCollectionAccessor();
await dbItemsEntry.LoadAsync();
var dbItemsMap = ((IEnumerable<object>)dbItemsEntry.CurrentValue)
.ToDictionary(e => string.Join('|', _context.FindPrimaryKeyValues(e)));
foreach (var item in (IEnumerable)accessor.GetOrCreate(entity))
{
if (!dbItemsMap.TryGetValue(string.Join('|', _context.FindPrimaryKeyValues(item)), out object oldItem))
{
accessor.Add(dbEntity, item);
}
else
{
_context.Entry(oldItem).CurrentValues.SetValues(item);
dbItemsMap.Remove(string.Join('|', _context.FindPrimaryKeyValues(item)));
}
}
foreach (var oldItem in dbItemsMap.Values)
{
accessor.Remove(dbEntity, oldItem);
await DeleteAsync(oldItem as IEntity, false);
}
}
if (save)
{
await SaveChangesAsync();
}
return entity;
}
上下文:
public IReadOnlyList<IProperty> FindPrimaryKeyProperties<T>(T entity)
{
return Model.FindEntityType(entity.GetType()).FindPrimaryKey().Properties;
}
public IEnumerable<object> FindPrimaryKeyValues<TEntity>(TEntity entity) where TEntity : class
{
return from p in FindPrimaryKeyProperties(entity)
select entity.GetPropertyValue(p.Name);
}
答案 2 :(得分:1)
最简单的方法是只获取所有Deleted
个实体,将它们转换为BaseEntity
,然后将其ID检查为实体关系集合中的当前ID。
类似的东西:
foreach (var property in navigations)
{
var propertyName = property.GetPropertyAccess().Name;
await dbEntry.Collection(propertyName).LoadAsync();
// this line specifically might need some changes
// as it may give you ICollection<SomeType>
var currentCollectionType = property.GetPropertyAccess().PropertyType;
var deletedEntities = _dbContext.ChangeTracker
.Entries
.Where(x => x.EntityState == EntityState.Deleted && x.GetType() == currentCollectionType)
.Select(x => (BaseEntity)x.Id)
.ToArray();
List<BaseEntity> dbChilds = dbEntry.Collection(propertyName).CurrentValue.Cast<BaseEntity>().ToList();
foreach (BaseEntity child in dbChilds)
{
if (child.Id == 0)
{
_dbContext.Entry(child).State = EntityState.Added;
}
if (deletedEntities.Contains(child.Id))
{
_dbContext.Entry(child).State = EntityState.Deleted;
}
else
{
_dbContext.Entry(child).State = EntityState.Modified;
}
}
}