使用EF Core更新通用存储库上的父级和子级集合

Gon*_*eto 6 c# generics entity-framework-core generic-repository

说我有一Sale堂课:

public class Sale : BaseEntity //BaseEntity only has an Id  
{        
    public ICollection<Item> Items { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

和一Item类:

public class Item : BaseEntity //BaseEntity only has an Id  
{
    public int SaleId { get; set; }
    public Sale Sale { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

以及通用存储库(更新方法):

    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();
    }
Run Code Online (Sandbox Code Playgroud)

我在更新课堂Item上的收藏时遇到困难Sale。有了这个代码,我设法addmodify一个Item。但是,当我deleteUI图层上放置某些项目时,什么也不会删除。

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);
    }
Run Code Online (Sandbox Code Playgroud)

Iva*_*oev 8

显然,问题是要应用断开的实体的修改(否则,除了调用,您无需做任何其他事情SaveChanges),其中包含需要反映传递的对象中添加/删除/更新的项的集合导航属性。

EF Core不提供此类开箱即用的功能。它通过具有自动生成键的实体通过方法支持简单的upsert(插入或更新)Update,但是它不会检测和删除已删除的项目。

因此,您需要自己进行检测。加载现有项目是朝正确方向迈出的一步。您的代码的问题在于它没有考虑新项目,而是对从数据库中检索到的现有项目进行了无用的状态操作。

以下是相同想法的正确实现。它使用一些EF Core内部构件(IClrCollectionAccessorGetCollectionAccessor()方法返回-都需要using Microsoft.EntityFrameworkCore.Metadata.Internal;)来操纵集合,但是您的代码已经在使用内部GetPropertyAccess()方法,所以我想这应该不成问题-以防将来的EF中发生某些更改核心版本,代码应相应更新。需要收集访问,因为虽然IEnumerable<BaseEntity>可以用于一般访问,由于协方差集合,同样不能说有关ICollection<BaseEntity>,因为它是不变的,我们需要一种方法来访问Add/ Remove方法。内部访问器提供了该功能以及从传递的实体中一般检索属性值的方法。更新:从EF Core 3.0开始,GetCollectionAccessorIClrCollectionAccessor是公共API的一部分。

这是代码:

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();
}
Run Code Online (Sandbox Code Playgroud)

该算法非常标准。从数据库加载集合后,我们创建一个字典,其中包含由ID键入的现有项(用于快速查找)。然后,我们对新项目进行一次传递。我们使用字典查找对应的现有项目。如果未找到匹配项,则该项目被视为新项目,并且仅添加到目标(跟踪)集合中。否则,找到的项目将从源中更新,并从字典中删除。这样,在完成循环之后,词典中包含需要删除的项目,因此我们所需要做的就是将它们从目标(跟踪)集合中删除。

就这样。剩下的工作将由EF Core变更跟踪器完成-将添加到目标集合的项目标记为Added,将其标记为更新- UnchangedModified,或者将标记为删除的项目,具体取决于删除级联行为删除或更新(与父级解除关联)。如果要强制删除,只需替换

accessor.Remove(dbEntity, oldItem);
Run Code Online (Sandbox Code Playgroud)

_dbContext.Remove(oldItem);
Run Code Online (Sandbox Code Playgroud)

  • 这两种方法都有效,只是上下文只有一个参数(愚蠢的错字 - 感谢您指出,更正)。但无论如何,从集合中删除在技术上看起来更正确。很高兴你喜欢它,干杯:) (2认同)
  • @IvanStoev自EF Core 3.0起,GetCollectionAccessor()是公共API的一部分:https://docs.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.navigationextensions.getcollectionaccessor?view=efcore-3.0 (2认同)

Jul*_*ida 5

@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;
    }
Run Code Online (Sandbox Code Playgroud)

语境:

 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);
        }
Run Code Online (Sandbox Code Playgroud)