【问题标题】:Update parent and child collections on generic repository with EF Core使用 EF Core 更新通用存储库上的父集合和子集合
【发布时间】:2019-03-10 14:55:14
【问题描述】:

假设我有一个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();
    }

我在更新 Sale 类上的 Item 集合时遇到了困难。使用此代码,我设法addmodifyItem。但是,当我 delete UI 层上的某个项目时,没有任何内容被删除。

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);
    }

【问题讨论】:

    标签: c# generics entity-framework-core


    【解决方案1】:

    显然,问题是应用修改 disconnected entity(否则除了调用 SaveChanges 之外您不需要做任何其他事情)包含需要反映传递对象中添加/删除/更新项目的集合导航属性.

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

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

    以下是相同想法的正确实现。它使用一些 EF Core 内部(GetCollectionAccessor() 方法返回的IClrCollectionAccessor - 两者都需要using Microsoft.EntityFrameworkCore.Metadata.Internal;)来操作集合,但是您的代码已经在使用内部GetPropertyAccess() 方法,所以我想这不应该是一个问题 - 万一在未来的 EF Core 版本中发生了一些变化,代码应该相应地更新。集合访问器是必需的,因为虽然 IEnumerable&lt;BaseEntity&gt; 由于协方差可用于一般地访问集合,但不能对 ICollection&lt;BaseEntity&gt; 说同样的话,因为它是不变的,我们需要一种访问 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();
    }
    

    算法非常标准。从数据库加载集合后,我们创建一个字典,其中包含以 Id 为键的现有项目(用于快速查找)。然后我们对新项目进行一次遍历。我们使用字典来查找对应的现有项。如果未找到匹配项,则该项目被视为新项目并简单地添加到目标(跟踪)集合中。否则,从源中更新找到的项目,并从字典中删除。这样,在完成循环后,字典中包含需要删除的项目,因此我们只需将它们从目标(跟踪)集合中删除即可。

    仅此而已。其余工作将由 EF Core 更改跟踪器完成 - 添加到目标集合的项目将标记为 Added,更新的项目 - UnchangedModified,以及删除的项目,具体取决于删除级联行为将被标记为删除或更新(与父级解除关联)。如果要强制删除,只需替换

    accessor.Remove(dbEntity, oldItem);
    

    _dbContext.Remove(oldItem);
    

    【讨论】:

    • 工作就像一个魅力!!! Remove 没有超载,但 accessor.Remove(...) 完成了这项工作。
    • 两种方法都有效,只是上下文一个只有一个参数(愚蠢的错字 - 感谢您指出,已更正)。但无论如何,从集合中删除在技术上看起来更正确。很高兴你喜欢它,干杯:)
    • @craigmoliver 我在一个新答案中回答了你的问题(评论字符太多),见下文
    • @IvanStoev 由于 EF Core 3.0,GetCollectionAccessor() 是公共 API 的一部分:docs.microsoft.com/en-us/dotnet/api/…
    • @Ajt 不,因为我们正在处理TEntity(例如Person.Blogs)的子集合,其中元素类型不同。需要这里的基本实体才能访问Id 属性。接口可以一般使用,但不像你的那样通用,即这里((IEnumerable&lt;BaseEntity&gt;)dbItemsEntry.CurrentValue).ToDictionary(e =&gt; e.Id) 我们需要转换为具有已知类型Id 属性的非泛型。您的方案需要的方法与此处的方法略有不同 - 可能是在通用方法中移动部分代码并通过反射调用它。
    【解决方案2】:

    @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);
            }
    

    【讨论】:

    • 如果我的班级有 2 组集合,那么如何调用这个函数?..谢谢
    【解决方案3】:

    最简单的方法是获取所有 Deleted 实体,将它们强制转换为 BaseEntity,然后将它们的 ID 与实体关系集合中的当前 ID 进行核对。

    类似的东西:

    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;
            }
        }
    }
    

    【讨论】:

    • deletedEntities 数组没有项目,即使我删除了Type 条件
    • 查看ChangeTracker 我注意到Item 对象是状态Unchanged
    • @GonzaloLorieto 这假设你调用了_dbContext.Remove(item),它用那些被删除的实体填充了ChangeTracker
    猜你喜欢
    • 1970-01-01
    • 2019-01-03
    • 1970-01-01
    • 1970-01-01
    • 2021-04-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多