Очевидно, что вопрос заключается в применении модификаций отключенного объекта (в противном случае вам не нужно будет делать ничего, кроме вызова 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);