Обновите родительские и дочерние коллекции в общем хранилище с помощью EF Core
Скажи у меня есть 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
трекинг потерян. Вот мой общий метод извлечения с использованием include.
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);
}
2 ответа
Очевидно, вопрос заключается в применении модификаций отключенной сущности (иначе вам не нужно будет делать ничего, кроме вызова SaveChanges
) содержащие свойства навигации по коллекции, которые должны отражать добавленные / удаленные / обновленные элементы из переданного объекта.
EF Core не предоставляет такую возможность из коробки. Он поддерживает простой upsert (вставить или обновить) через Update
метод для сущностей с автоматически сгенерированными ключами, но он не обнаруживает и не удаляет удаленные элементы.
Так что вам нужно сделать это обнаружение самостоятельно. Загрузка существующих предметов - это шаг в правильном направлении. Проблема с вашим кодом в том, что он не учитывает новые элементы, а вместо этого выполняет некоторые бесполезные манипуляции с состоянием существующих элементов, извлеченных из базы данных.
Ниже приводится правильная реализация той же идеи. Он использует некоторые внутренние компоненты EF Core (IClrCollectionAccessor
вернулся GetCollectionAccessor()
метод - оба требуют using Microsoft.EntityFrameworkCore.Metadata.Internal;
) манипулировать коллекцией, но ваш код уже использует внутренний GetPropertyAccess()
метод, поэтому я думаю, что это не должно быть проблемой - в случае, если что-то изменится в какой-то будущей версии EF Core, код должен быть обновлен соответствующим образом. Доступ к коллекции необходим, потому что пока IEnumerable<BaseEntity>
может использоваться для общего доступа к коллекциям из-за ковариации, чего нельзя сказать о ICollection<BaseEntity>
потому что это инвариант, и нам нужен способ доступа Add
/ Remove
методы. Внутренний метод доступа обеспечивает эту возможность, а также способ общего получения значения свойства из переданного объекта.
Вот код:
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();
}
Алгоритм довольно стандартный. После загрузки коллекции из базы данных мы создаем словарь, содержащий существующие элементы с ключом Id (для быстрого поиска). Затем мы делаем один проход для новых предметов. Мы используем словарь, чтобы найти соответствующий существующий элемент. Если совпадений не найдено, элемент считается новым и просто добавляется в целевую (отслеживаемую) коллекцию. В противном случае найденный элемент обновляется из источника и удаляется из словаря. Таким образом, после завершения цикла словарь содержит элементы, которые необходимо удалить, поэтому все, что нам нужно, это удалить их из целевой (отслеживаемой) коллекции.
И это все. Остальная часть работы будет выполнена с помощью трекера изменений EF Core - добавленные элементы в целевую коллекцию будут отмечены как Added
Обновленный - либо Unchanged
или же Modified
и удаленные элементы, в зависимости от поведения каскада удаления, будут помечены либо для удаления, либо для обновления (отсоединиться от родителя). Если вы хотите принудительно удалить, просто замените
accessor.Remove(dbEntity, oldItem);
с
_dbContext.Remove(oldItem);
@craigmoliver Вот мое решение. Это не самый лучший, я знаю - если вы найдете более элегантный способ, пожалуйста, поделитесь.
Repository:
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);
}
Проще всего было бы просто получить все Deleted
сущности, бросьте их BaseEntity
и проверьте их идентификаторы на текущие идентификаторы в коллекции отношений объекта.
Что-то вроде:
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;
}
}
}