Обновите родительские и дочерние коллекции в общем хранилище с помощью EF Core - PullRequest
5 голосов
/ 10 марта 2019

Скажем, у меня есть 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);
    }

Ответы [ 3 ]

6 голосов
/ 16 марта 2019

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

@ 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);
        }
1 голос
/ 10 марта 2019

Самым простым было бы просто получить все 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;
        }
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...