Обновите родительские и дочерние коллекции в общем хранилище с помощью 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;
        }
    }
}
Другие вопросы по тегам